“Build a modal” is one of the most common frontend interview questions. It looks simple. It is not. The interview probes whether you understand focus trapping, ARIA roles, animation choreography, and the dozens of accessibility details that separate a polished modal from a broken one.
Functional requirements
- Trigger button opens the modal
- Modal renders on top of page content
- Click outside (overlay) closes
- Escape key closes
- Focus traps inside the modal
- Returns focus to the trigger on close
- Animation in/out
- Screen reader friendly
The native dialog element
Modern browsers have <dialog> with showModal():
<dialog id="myModal">...</dialog>
<script>
document.getElementById('myModal').showModal();
</script>
Built-in: top layer rendering, focus trap, Escape close. Use this if your styling needs allow.
Custom modal architecture
If <dialog> is not enough:
- Render to a portal (top-level DOM node, not nested inside content)
- Position absolutely; full viewport overlay
- Inert content underneath (inert attribute)
- Focus trap inside modal
- Return focus to trigger on close
Focus trap
When modal is open, Tab cycles between focusable elements inside the modal. Shift+Tab cycles backward.
Implementation:
- Find first and last focusable element
- On Tab from last → focus first
- On Shift+Tab from first → focus last
Use a library: focus-trap, react-focus-lock. Don’t hand-roll unless edge cases require.
Returning focus
On modal close, focus returns to the element that opened it (typically the trigger button). Without this, keyboard users get stranded.
Implementation:
- Capture
document.activeElementwhen modal opens - Call
element.focus()on close
ARIA
role="dialog"(orrole="alertdialog"for urgent confirmations)aria-modal="true"aria-labelledbypointing to the titlearia-describedbypointing to the description
Inert background
The inert attribute on the page content makes it non-interactive while the modal is open. Modern browsers support; older browsers need polyfill or alternative.
Without inert, screen readers and tab navigation can leak into background content.
Animation
Common patterns:
- Fade in overlay (300ms)
- Slide-up dialog from below or scale in
- On close: reverse the animation
Animate transform and opacity (compositor-friendly). Avoid animating top/left or width/height.
Click outside to close
Click on overlay → close. Click on dialog → do not close.
Implementation: stopPropagation on dialog click; close handler on overlay click.
Watch out: drag from inside dialog to outside should not close. Track mousedown vs mouseup correctly.
Escape to close
Listen for Escape key. Close modal. Native <dialog> handles automatically.
Stacking modals
If a modal can open another modal, the second covers the first. Each maintains its own focus trap. On close, the deeper modal returns focus to the trigger in the first modal.
Most apps avoid stacking — UX gets confusing.
Mobile-specific
- Bottom sheets (iOS-style) instead of centered modals
- Swipe-down-to-close gesture
- Full-screen on small viewports
Common mistakes
- No focus trap — keyboard users tab into background content
- No focus return — closing modal strands the user
- Background not inert — screen readers read the wrong content
- Click handler on dialog itself triggers close
- Animation does not respect prefers-reduced-motion
Frequently Asked Questions
Should I use the native dialog element?
Yes when possible. Native handles many edge cases. Fall back to custom for cases where styling or behavior needs more control.
How do I handle a confirmation dialog within a modal?
Stack two dialogs. The inner is role=”alertdialog”. Trap focus in the inner; return to the outer on close.
How does this work with React?
Use a portal (createPortal) to render the modal at the top of the DOM tree. Use a library (Radix Dialog, React Aria) for accessibility heavy lifting.