Machine coding

Build an Accessible Modal / Dialog

← Back to Machine coding

Difficulty: 🟡 Medium · Est. time: 45m · Tags: #overlay #a11y #focus

Asked at: Google, Meta, Airbnb, Atlassian · Related: Command Palette · Autocomplete


1. The Question

Build a reusable modal dialog: opens over the page, traps focus, closes on Esc / backdrop click, and is fully accessible.

2. Requirements

Functional

Non-functional

3. Component API

type ModalProps = {
  isOpen: boolean;
  onClose: () => void;
  title: string;          // for aria-labelledby
  children: React.ReactNode;
  initialFocusRef?: React.RefObject<HTMLElement>;
};

4. Implementation Notes & Trade-offs

Portal → render into document.body via createPortal so the modal escapes parent overflow/transform/z-index stacking contexts. This is the #1 reason naive modals get clipped.

ARIA semanticsrole="dialog" (or alertdialog), aria-modal="true", aria-labelledby → the title, aria-describedby → the body. This tells screen readers it's a modal context.

Focus management (the graded part)

  • On open: move focus to the first focusable element (or initialFocusRef, or the dialog itself).
  • Trap focus: intercept Tab / Shift+Tab so focus cycles within the dialog; query focusable elements and wrap around at the ends.
  • On close: return focus to the element that opened it (save document.activeElement on open).

Make the background inert → ideally set inert on the rest of the page (or aria-hidden="true" on siblings) so AT and Tab can't reach background content. Lock body scroll (overflow: hidden) and account for scrollbar width to avoid layout shift.

Closing → Esc key (keydown listener), backdrop click (but not clicks that start inside and drag out — check target === currentTarget on the backdrop). Clean up listeners on unmount.

Animation → animate opacity/transform; keep the node mounted during exit transition, then unmount. Respect prefers-reduced-motion.

Edge cases → nested/stacked modals (focus-trap stack), no focusable children (focus the dialog), content taller than viewport (scroll inside the dialog), opening while another is open.

Pro move: mention the platform primitive <dialog> + showModal() handles focus trap, backdrop, and inert natively — and when you'd still hand-roll (older browser support, custom behavior).

5. What Interviewers Probe

  • Why a portal?
  • The full focus lifecycle: move in → trap → return.
  • How you trap Tab/Shift+Tab.
  • Making the background inert + scroll lock (+ scrollbar shift).
  • Backdrop-click vs drag-out; Esc handling.
  • ARIA: role="dialog", aria-modal, labelling.
  • The native <dialog> element trade-off.

6. Curated Resources

ESC

Type to search across every question bank.

navigate open 3,610 questions indexed