Build a Modal
Render into a portal, trap focus inside while open and restore it on close, close on Escape and overlay click, lock body scroll, use role=dialog + aria-modal + aria-labelledby, and support controlled open/close. Accessibility is the hard part, not the visuals.
A modal is the canonical "looks easy, accessibility is hard" component. The visuals are trivial; focus management and ARIA are the real work.
1. Portal — render outside the component tree
createPortal(<div className="overlay">...</div>, document.body);Rendering into document.body (not nested in the parent) avoids overflow: hidden / z-index / stacking-context clipping issues. The modal still participates in React state/context normally.
2. Focus management — the part interviewers grade
- On open: move focus into the modal (the first focusable element, or the modal container).
- Focus trap: Tab / Shift+Tab must cycle within the modal — you can't tab to the page behind it. Implement by intercepting Tab at the first/last focusable element and wrapping.
- On close: restore focus to the element that opened the modal (save
document.activeElementon open). Forgetting this strands keyboard/screen-reader users.
3. Closing behaviors
- Escape key closes.
- Overlay (backdrop) click closes — but a click inside the modal must not (stop propagation / check the target).
- A close button.
- Sometimes "are you sure?" for forms with unsaved changes.
4. Body scroll lock
While open, lock background scroll (overflow: hidden on <body>, ideally compensating for scrollbar width to avoid a layout shift). Restore on close.
5. ARIA
role="dialog"(or"alertdialog"),aria-modal="true".aria-labelledby→ the modal title's id;aria-describedby→ the body if useful.- The rest of the page can be marked
aria-hidden/inertwhile the modal is open so screen readers don't wander behind it.
6. API
- Controlled —
isOpen/onCloseprops; the parent owns visibility. - Render
childrenas the content; maybe slots/compound components for header/body/footer. - Don't render (or unmount) when closed — or animate exit before unmounting.
7. Extras
- Enter/exit animations (animate out before unmounting).
<dialog>element +showModal()now gives focus trap, backdrop, and Escape natively — worth mentioning as the modern primitive, though styling/animation still need care.
How to answer
"Render it in a portal to document.body to escape clipping. The hard part is focus: move focus in on open, trap Tab inside while open, and restore focus to the trigger on close. Close on Escape and backdrop click (but not inner clicks), lock body scroll, and set role=dialog + aria-modal + aria-labelledby. Controlled isOpen/onClose API. In production I'd use a headless library or the native <dialog> element since this is easy to get subtly wrong."
Follow-up questions
- •Why render a modal in a portal?
- •How do you implement a focus trap?
- •Why must you restore focus to the trigger on close?
- •What does the native <dialog> element give you for free?
Common mistakes
- •No focus trap — users can tab to the page behind the modal.
- •Not restoring focus to the trigger on close.
- •Backdrop click closing even when the click was inside the modal.
- •Forgetting body scroll lock, or causing a layout shift from it.
- •Missing role=dialog / aria-modal / aria-labelledby.
Performance considerations
- •Unmount the modal when closed (or after exit animation) to free its subtree. Portaling avoids reflow from deep nesting. Scrollbar-width compensation on body lock prevents a visible jump.
Edge cases
- •Nested modals (focus trap and Escape ordering).
- •Very tall modal content needing internal scroll.
- •Modal opened from a context with unsaved form data.
- •Exit animation needing to finish before unmount.
Real-world examples
- •Confirmation dialogs, image lightboxes, forms-in-modals; Radix Dialog, Headless UI Dialog.