Accessible Modals Patterns
Modal dialogs that work: focus trap, Escape, return focus, aria-modal, and why native dialog or APG patterns beat div soup.
- accessibility
- accessible-modals
Modals fail accessibility when focus leaks to the page behind, Escape does nothing, and the title isn’t announced. Treat a modal as a mode change: move focus in, trap while open, restore on close.
Docs: APG Dialog, HTML dialog.
Prefer native <dialog>
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">Delete file?</h2>
<p>This cannot be undone.</p>
<button type="button" id="cancel">Cancel</button>
<button type="button" id="ok">Delete</button>
</dialog>
const dialog = document.getElementById('confirm');
const opener = document.getElementById('delete-btn');
opener.addEventListener('click', () => {
dialog.showModal();
});
document.getElementById('cancel').addEventListener('click', () => {
dialog.close();
});
dialog.addEventListener('close', () => {
opener.focus();
});
showModal() provides a top layer and basic focus handling in modern browsers. Still label the dialog and restore focus.
Required behaviors (custom or native)
- Open: focus dialog container (
tabindex="-1") or first focusable control. - Tab/Shift+Tab: cycle inside the dialog.
- Escape: close (unless destructive workflow explicitly needs confirm — still usually allow Escape to cancel).
- Close: return focus to opener.
- Background: inert / non-interactive (
aria-hiddenorinerton the rest of the page when not using native modal).
<div
role="dialog"
aria-modal="true"
aria-labelledby="title"
tabindex="-1"
>
<h2 id="title">Settings</h2>
…
</div>
Labeling
aria-labelledby→ visible title- or
aria-labelif no visible title (prefer visible) - Optional
aria-describedbyfor supporting text
Initial focus choice
| Dialog type | Initial focus |
|---|---|
| Confirm destructive | Cancel (safer) or least destructive action |
| Form modal | First field |
| Info only | Close button or container |
Scroll and background
Lock body scroll carefully; ensure focusable elements under the overlay aren’t reachable. Native modal dialog helps.
React note
Portals render dialogs at document.body. Manage focus in effects; cleanup on unmount. Libraries: Radix Dialog, React Aria — still verify.
Footguns
role="dialog"without focus trap.- Focus on close goes to
body. - Nested modals without stack discipline.
- Auto-opening modals on page load.
- Only click-outside to close — keyboard users need Escape.
Interview out-loud answer
“Modals need labeled dialog semantics, focus moved in, Tab trapped, Escape to close, and focus restored to the opener. I prefer native dialog showModal when possible. aria-modal documents the modality; focus control makes it real.”
Background inert
When not using showModal(), mark the rest of the page inert or aria-hidden carefully (don’t hide the dialog itself). Ensure assistive tech cannot read “through” the overlay. Test that Tab never lands on a footer link behind the mask.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Notes from real codebases
Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.
Related on this site
- Focus management
- Skip links
- Accessible tabs pattern
- ARIA roles overview
- Keyboard accessibility checklist
Further reading
Related guides
- Accessible Combobox PatternBuild or evaluate comboboxes with APG keyboard behavior, aria-expanded/activedescendant, and filterable listbox pairing.
- Accessible Forms ErrorsLabel inputs, associate errors with fields, announce failures, and avoid placeholder-only forms that break accessibility.
- Accessible Menus PatternAPG menu and menubar keyboard model — arrow navigation, escape to close, aria-expanded, and when a disclosure is enough.
- Accessible Names ComputationHow browsers compute accessible names from content, labels, aria-label, and labelledby — and how that powers Testing Library queries.
- Accessible SVGsDecorative vs informative SVG icons — aria-hidden, title/desc, role=img, and pairing icons with accessible names on controls.