Machine Coding
Modal
Master building a custom Modal component in React, covering React Portals, focus trapping, keyboard shortcuts, backdrop clicks, and enter/exit animations.
1. Learning Objectives
In this challenge, you will build a custom Modal component from scratch. By the end of this guide, you will be able to:
- Use
ReactDOM.createPortalto render modal content outside the parent DOM hierarchy. - Implement focus trapping to keep keyboard navigation inside the modal.
- Handle keyboard shortcuts (ESC to close).
- Close the modal when the user clicks the backdrop overlay.
- Add CSS enter/exit animations for smooth transitions.
2. Overview
A Modal is one of the most frequently asked machine coding questions because it combines multiple React concepts: portals for DOM placement, refs for focus management, effects for event listener cleanup, and conditional rendering with animations. Building one from scratch demonstrates mastery over React's escape hatches.
3. Why This Challenge Matters
Custom modals teach critical UI interaction patterns:
- React Portals: Rendering outside the parent DOM tree solves z-index and overflow issues that plague modals rendered inline.
- Accessibility: Focus trapping ensures keyboard users cannot tab behind the modal, which is a WCAG requirement for accessible overlays.
4. Real-World Analogy
Think of a Modal like a pop-up window at a bank counter:
- The Portal: The pop-up window is not part of the regular queue line — it appears separately in front of everything. This is why modals use portals to render outside the normal DOM tree.
- The Backdrop: A frosted glass partition separates the window from the rest of the bank. Tapping the partition closes the window — this is the backdrop click handler.
- Focus Trapping: Once you step up to the window, you can only interact with the teller until the conversation is done. You cannot walk away mid-transaction — this is focus trapping.
5. Core Concepts
Below is a comparison of inline rendering versus portal rendering for overlays:
| Property | Inline Rendering | Portal Rendering |
|---|---|---|
| DOM Placement | Modal HTML is nested inside the parent component's DOM tree. | Modal HTML is rendered into a separate DOM node (e.g., #modal-root). |
| Z-index Issues | Parent overflow: hidden or stacking contexts can clip or hide the modal. |
No clipping. The modal sits outside all parent stacking contexts. |
| Event Bubbling | Events bubble through the React tree normally. | Events still bubble through the React tree (not DOM tree), so portal children behave as expected in React. |
6. Syntax & API Reference
This example shows how to create a portal-based modal with ESC key handling:
7. Visual Diagram
This diagram shows how a portal renders modal content outside the parent DOM tree:
8. Live Example — Full Working Code
Below is a complete HTML page demonstrating a portal-based modal with backdrop click, ESC key, and animations:
What just happened? The Modal component renders its content into #modal-root using ReactDOM.createPortal. Clicking the backdrop closes the modal. Pressing ESC closes it via a keydown listener. The e.stopPropagation() call on the modal box prevents clicks inside the modal from triggering the backdrop's close handler.
9. Interactive Playground
Try It Yourself Challenges:
- Implement full focus trapping: when the user presses Tab on the last focusable element inside the modal, cycle focus back to the first focusable element.
- Add an exit animation: when closing the modal, play a fade-out animation before removing it from the DOM.
10. Common Mistakes
| Mistake | Why it happens | Wrong | Correct |
|---|---|---|---|
| Clicking inside modal closes it | The click event on the modal content bubbles up to the backdrop's onClick handler. |
<div onClick={onClose}> |
<div onClick={onClose}> |
| Not cleaning up event listeners | Adding a keydown listener for ESC without removing it in the useEffect cleanup causes memory leaks and stale closures. |
useEffect(() => { |
useEffect(() => { |
11. Best Practices
- Use portals for overlays: Always render modals into a separate DOM node to avoid z-index and overflow clipping issues.
- Lock body scroll: Set
document.body.style.overflow = 'hidden'when the modal opens and restore it on close. - Add ARIA attributes: Use
role="dialog",aria-modal="true", andaria-labelledbyfor screen reader accessibility. - Auto-focus the modal: Set
tabIndex={-1}on the modal container and call.focus()on mount to establish focus context.
12. Browser Compatibility/Requirements
React portals (ReactDOM.createPortal) are supported in React 16+ and work on all modern browsers.
13. Interview Questions
Q1: Why do React portals preserve event bubbling through the React tree even though the DOM node is elsewhere?
Answer:
React maintains its own synthetic event system that is based on the React component tree, not the DOM tree. A portal child is still a child in the React tree, so events bubble up through React's virtual hierarchy. This means a click inside a portal can still trigger an onClick on a parent React component, even though the DOM elements are in different branches.
Q2: What is focus trapping and why is it important for modals?
Answer: Focus trapping confines keyboard focus (Tab and Shift+Tab) to elements within the modal while it is open. This prevents users from tabbing to elements behind the modal overlay, which would be visually confusing and violates WCAG accessibility guidelines. When the modal closes, focus should be returned to the element that triggered it.
14. Debugging Exercise
Identify why clicking the "Cancel" button inside the modal closes the modal AND triggers the delete action:
Diagnosis: Even though the modal is rendered via a portal (in a different DOM location), React's synthetic event system bubbles events through the React component tree. The Cancel button's click bubbles up through the React tree to the parent <div onClick={deleteItem}>, triggering the delete.
Fix: Stop propagation on the modal content, or restructure the parent so the delete handler is not a parent of the modal component:
15. Practice Exercises
Exercise 1: Stacked modals
Implement a system that supports multiple stacked modals. Opening a second modal from within the first should dim the first modal further. Pressing ESC should close only the topmost modal.
16. Scenario-Based Challenge
The unsaved changes confirmation challenge:
Your form modal contains unsaved changes. When the user clicks the backdrop or presses ESC, instead of closing immediately, show a nested confirmation dialog: "You have unsaved changes. Discard?" with "Keep Editing" and "Discard" buttons. Describe how you would manage the two modal states and prevent the outer modal from closing until the user explicitly confirms.
17. Quick Quiz
Q1: What does e.stopPropagation() prevent in the context of a modal backdrop?
A) It prevents the modal from rendering
B) It prevents clicks inside the modal content from bubbling to the backdrop's onClick handler
C) It prevents the browser from navigating
Answer: B — Without stopPropagation, clicking a button inside the modal would also trigger the backdrop's close handler, because the click event bubbles up through the DOM.
18. Summary & Key Takeaways
- Use
ReactDOM.createPortalto render modals outside the parent DOM hierarchy. - Stop event propagation on the modal content box to prevent backdrop close on internal clicks.
- Add ESC key handling and clean up event listeners in useEffect.
- Lock body scroll and set ARIA attributes for accessibility.
- Auto-focus the modal container on open and return focus to the trigger on close.
19. Cheat Sheet
| Operation | Syntax Pattern |
|---|---|
| Create portal | ReactDOM.createPortal(jsx, document.getElementById('modal-root')) |
| Stop propagation | onClick={e => e.stopPropagation()} |
| ESC handler | if (e.key === 'Escape') onClose() |
| Lock scroll | document.body.style.overflow = 'hidden' |