The dialog Element
Native modal and non-modal dialogs with showModal, light dismiss, focus, and the top layer — without a heavyweight widget library.
- html
- dialog
- modal
- accessibility
The HTML <dialog> element is the platform’s answer to “please build an accessible modal.” It gives you open/close semantics, a top layer, focus handling hooks, and form method dialog — without inventing role="dialog" from scratch (though you still must design content carefully).
Docs: MDN <dialog>, HTML living standard — dialog, Open UI / modal patterns.
Modal vs non-modal
<dialog id="confirm">
<form method="dialog">
<p>Delete this project? This cannot be undone.</p>
<menu>
<button value="cancel" type="submit">Cancel</button>
<button value="confirm" type="submit">Delete</button>
</menu>
</form>
</dialog>
<button type="button" id="open">Delete project</button>
const dialog = document.querySelector('#confirm');
document.querySelector('#open').addEventListener('click', () => {
dialog.showModal(); // modal: top layer + backdrop + inert-ish page
});
dialog.addEventListener('close', () => {
console.log(dialog.returnValue); // "cancel" | "confirm" | ""
});
| API | Behavior |
|---|---|
showModal() |
Modal dialog on the top layer, with ::backdrop, traps focus in the dialog (browser-managed), rest of page non-interactive |
show() |
Non-modal dialog; page remains interactive; no top-layer modal behavior |
close(returnValue?) |
Closes; sets returnValue |
open property / attribute |
Reflects open state (prefer APIs for modal) |
Use modal for confirmations, blocking forms, and anything that must capture the task. Use non-modal sparingly (e.g. persistent inspector); many “non-modal dialogs” are better as inline disclosure or popovers.
Why not a centered div?
Hand-rolled modals routinely miss:
- Top layer — paints above
z-indexwars without stacking-context archaeology (position / stacking). - Focus — move focus in on open, restore on close; Tab cycles inside.
- Escape to close (modal dialogs).
- Backdrop click policies.
inertbackground content so AT doesn’t walk the page under the mask.
<dialog> covers a large chunk of that matrix. You still supply titles, labels, and clear actions.
Accessible naming
<dialog id="edit" aria-labelledby="edit-title" aria-describedby="edit-desc">
<h2 id="edit-title">Edit profile</h2>
<p id="edit-desc">Changes apply to your public profile.</p>
<!-- fields -->
<button type="button" id="edit-close">Close</button>
</dialog>
- Give the dialog an accessible name (
aria-labelledbyto a visible heading is ideal). - Put the initial focus on a sensible control (first field or primary action) — browsers focus the dialog; refine if needed.
- Ensure a clear way to dismiss (button + Escape for modals).
Forms and method="dialog"
<dialog id="nickname">
<form method="dialog">
<label for="nick">Nickname</label>
<input id="nick" name="nick" />
<button value="save">Save</button>
</form>
</dialog>
Submitting a form with method="dialog" closes the dialog and sets returnValue from the submitter’s value. The form is not navigated as a normal HTTP submit. Read field values in the close handler or before close if you need them — once closed, design your state flow explicitly in SPA frameworks.
Light dismiss (backdrop)
Backdrop clicks do not always close by default in all cases you might expect; check current browser behavior and implement deliberately:
dialog.addEventListener('click', (e) => {
const rect = dialog.getBoundingClientRect();
const inDialog =
rect.top <= e.clientY &&
e.clientY <= rect.bottom &&
rect.left <= e.clientX &&
e.clientX <= rect.right;
if (!inDialog) dialog.close('cancel');
});
(Or compare e.target === dialog depending on structure.) For destructive flows, prefer explicit Cancel over accidental backdrop dismiss.
Styling
dialog {
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.25rem;
max-width: min(28rem, 100vw - 2rem);
}
dialog::backdrop {
background: rgb(0 0 0 / 0.45);
}
dialog:not([open]) {
display: none; /* default; don’t fight it carelessly */
}
Avoid display: none hacks that break the open state. Animate with caution — respect prefers-reduced-motion. Fixed centering used to need absolute hacks; dialogs are centered by UA styles you can override.
Framework notes (React / Next)
- Control open state carefully: call
showModal()in an effect whenopenbecomes true; callclose()when false. Don’t only toggle anopenattribute if you need modal top-layer behavior. - Portals are less necessary for stacking because of the top layer, but app structure may still wrap dialogs at root for state.
- Sync with route segments (intercepting routes, parallel routes) if the URL should represent the modal — App Router pattern territory (App Router overview).
Common mistakes
- Using
show()when you neededshowModal(). - No accessible name / heading.
- Nested modals without a clear stack story.
- Forgetting to restore focus to the invoker on close (test if UA + your focus moves are enough).
- Trapping scroll on
bodymanually in ways that fight the browser. - Building “modals” for simple messages that should be inline errors or non-modal announcements.
Interview angle
Contrast showModal vs show. Mention top layer, Escape, and form method="dialog" / returnValue. Explain why native dialog beats a div + z-index: 9999. Tie to focus management and naming.
Live coding: confirmation modal with Cancel/Delete, keyboard Escape, and returnValue handling.
Related on this site
- Links vs buttons
- Forms, labels, and inputs
- Focus management
- Accessible modals patterns
- WCAG principles (POUR)
- Position and stacking
Further reading
- MDN:
<dialog> - MDN: Using the dialog element for confirmations
- HTML Living Standard —
dialog - web.dev: Building a dialog component (patterns; prefer native where enough)
Related guides
- Forms, Labels, and InputsWire labels to controls correctly, choose input types, group fields, and avoid the accessibility bugs that fail real users and audits.
- Links vs ButtonsNavigate with links, act with buttons — correct semantics, keyboard behavior, and the SPA footguns that break both.
- Accessibility Tree OverviewHow browsers build the accessibility tree from DOM and CSS — roles, names, states, what’s pruned, and how to inspect it in DevTools.
- Audio and Video ElementsNative audio/video — controls, sources, captions, autoplay policies, and accessibility requirements for media on the web.
- Autocomplete and Name Attributesname and autocomplete on form fields — password managers, autofill tokens, and why missing names break real users more than demos.