Native Overlays & Dialogs
Custom modal libraries reimplement focus traps, Escape handling, backdrop clicks, and aria-modal. The platform now
covers much of that with <dialog>, invoker attributes, and CSS for open/closed styling.
For tooltips and anchored menus, prefer Anchor Positioning & Popover. This page focuses on dialogs and command invokers.
<dialog> basics
<dialog id="confirm">
<form method="dialog">
<p>Discard unsaved changes?</p>
<button value="cancel">Keep editing</button>
<button value="confirm">Discard</button>
</form>
</dialog>
<button id="open-confirm">Discard…</button>
const dialog = document.querySelector('#confirm');
document.querySelector('#open-confirm').addEventListener('click', () => {
dialog.showModal();
});
dialog.addEventListener('close', () => {
console.log(dialog.returnValue); // "confirm" or "cancel"
});
| Method / behaviour | Role |
|---|---|
showModal() | Modal dialog + backdrop; focus moves into the dialog |
show() | Non-modal dialog (no backdrop; less common) |
close() | Closes; optional return value |
method="dialog" on <form> | Submitting a button closes the dialog and sets returnValue |
Style the backdrop with ::backdrop:
dialog {
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.25rem;
max-width: min(28rem, 100% - 2rem);
background: var(--surface);
color: var(--text);
}
dialog::backdrop {
background: rgb(0 0 0 / 0.45);
}
Open state and entry motion
Browsers expose open dialogs via the open attribute / :open where supported. Pair with
@starting-style and transition-behavior: allow-discrete for entry animations (see
Transitions & Animations):
dialog {
opacity: 0;
transform: translateY(0.5rem);
transition:
opacity 200ms ease,
transform 200ms ease,
overlay 200ms allow-discrete,
display 200ms allow-discrete;
}
dialog:open {
opacity: 1;
transform: none;
}
@starting-style {
dialog:open {
opacity: 0;
transform: translateY(0.5rem);
}
}
@media (prefers-reduced-motion: reduce) {
dialog {
transition: none;
}
}
Support for discrete transitions on display / overlay varies -- keep a usable unanimated dialog as the baseline.
Light dismiss
Modal dialogs traditionally close via explicit buttons or Escape. Some browsers are adding light dismiss
behaviours for dialogs (click outside) comparable to popovers -- verify current MDN notes for closedby /
light-dismiss attributes before relying on them. Portable pattern today:
dialog.addEventListener('click', (event) => {
if (event.target === dialog) {
dialog.close('cancel');
}
});
Put interactive content in an inner wrapper so clicks on the panel do not hit the <dialog> element itself:
<dialog id="confirm">
<div class="dialog-panel">…</div>
</dialog>
dialog {
padding: 0;
border: none;
background: transparent;
}
.dialog-panel {
padding: 1.25rem;
background: var(--surface);
border-radius: 0.75rem;
}
Invokers / commandfor (progressive)
HTML is gaining invoker commands so buttons can open dialogs declaratively (for example commandfor /
command="show-modal" -- names have evolved in proposals and early shipping). Treat declarative invokers as
progressive enhancement:
<button commandfor="confirm" command="show-modal">Discard…</button>
<dialog id="confirm">…</dialog>
If the attributes are missing in a browser, keep a few lines of showModal() as in the first example. Always check
MDN for the current attribute names and
support.
Dialog vs popover
| Need | Prefer |
|---|---|
| Modal flow, form confirm, focus trap, backdrop | <dialog showModal()> |
| Non-modal menu, tooltip, teaching bubble | popover (+ anchors) |
| Navigate away / route change | App-level routing, not a forever-open dialog |
You can combine patterns (a dialog that contains anchored popovers) but keep one clear primary modality for the user.
Accessibility checklist
- Prefer
showModal()for true modals so focus is trapped appropriately. - Give the dialog an accessible name (
aria-labelledbypointing at a heading, oraria-label). - Restore focus to the trigger on close when you move focus manually.
- Do not hide essential content only behind hover.
- Ensure backdrop contrast does not make the page look broken for zoom users -- content in the dialog must scroll if
needed (
max-height+overflow: autoon the panel).
Gotchas
- Centring. User-agent styles centre modal dialogs; override carefully with logical insets if you customise layout.
- Nested dialogs. Avoid stacking many modals; redesign the flow.
display: nonefights. Let the dialog API manage open/closed; animate with the patterns above instead of hand-rolling a second visibility system.- Forms.
method="dialog"buttons close without a network submit -- use regular forms when you need HTTP POST.
Related reading
- Anchor Positioning & Popover -- menus and tooltips
- Transitions & Animations --
@starting-style, discrete transitions - MDN:
<dialog> - Inspiration: modern-css.com modal dialogs