dialog
Marks a container as a modal or non-modal dialog. Use the native <dialog> element with showModal() — it handles focus trapping, the top-layer, the backdrop, and Escape-to-close for free. Reach for role="dialog" only when the native element is impossible.
When to use
Use <dialog> opened via .showModal(). The native element is supported in every current browser, handles the inert background, the focus trap, Escape-to-close, the ::backdrop styling hook, and the top-layer rendering. It saves you about 200 lines of fragile JavaScript.
Reach for role="dialog" on a <div> only when:
- You support browsers older than Chrome 37 / Firefox 98 / Safari 15.4 (unlikely in 2026).
- The design demands a dialog that cannot live in the top-layer (rare).
- You’re patching an inaccessible custom modal during a refactor.
Every dialog MUST have an accessible name (aria-labelledby pointing at a visible heading is the strongest pattern). Modal dialogs also need aria-modal="true".
Keyboard + focus contract
Per the APG dialog (modal) pattern:
- On open, focus moves to the first focusable element inside the dialog (often the close button, or the primary input).
- Tab and Shift+Tab cycle focus WITHIN the dialog — a “focus trap”. Tab from the last element returns to the first; Shift+Tab from the first returns to the last.
- Escape closes the dialog.
- On close, focus returns to the element that opened the dialog. Saving and restoring focus is non-negotiable; users who lose their place have no anchor.
- The rest of the page is
inert(oraria-hidden="true"+ non-focusable as a fallback) while the dialog is open.
Common failures
- Custom dialog with no focus trap. Tab steps out of the dialog and behind it; users lose context.
- Focus not restored to the trigger on close. The next Tab lands on the document body.
- Background content remains focusable through Tab even though it is visually hidden behind the dialog.
- No accessible name. The dialog opens and the screen reader announces only “dialog”.
- Custom dialog without
aria-modal="true"— assistive tech treats it as inline content. - Escape closes the dialog, but the dialog is rendered through a portal that loses keyboard focus during the close animation.
Example
<!-- Preferred -->
<dialog id="confirm">
<h2>Delete this draft?</h2>
<p>This cannot be undone.</p>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="delete" autofocus>Delete</button>
</form>
</dialog>
<script>
document.getElementById('confirm').showModal();
</script>
<!-- When <dialog> is impossible -->
<div role="dialog" aria-modal="true" aria-labelledby="dlgTitle">
<h2 id="dlgTitle">Edit profile</h2>
…
</div>