< dialog >
The dialog element: modals, built in
A native modal and non-modal dialog element. HTML gives you the top layer, focus trapping, inert background content, Escape-to-close handling, backdrop styling, and proper focus return when the dialog closes. The dialog element has been shipping in all three major browser engines since early 2022 and has matured meaningfully in the years since.
Built-in with native HTML
- Top-layer rendering. A modal dialog renders above every other element on the page, including fixed-position elements inside transformed ancestors. The top layer is above all z-index stacking contexts.
- Focus trapping. Tab and Shift-Tab cycle inside the dialog while it's open. The browser handles this.
- Background inertness. Content outside the dialog is non-interactive while the dialog is modal. Pointer events, clicks, and keyboard focus all stay inside.
- Escape-to-close. Pressing Escape closes a modal dialog and fires the
closeevent. - Focus return. When the dialog closes, focus goes back to whichever element was focused when the dialog opened.
- The
::backdroppseudo-element. The area behind a modal dialog is a real pseudo-element that CSS can style. - Semantic role. The element is announced as a dialog with modal or non-modal state, without any ARIA attributes.
Modal vs non-modal: two open modes, two different behaviors
The dialog element has two JavaScript methods for opening, and they produce genuinely different user experiences.
| Behavior | show() (non-modal) |
showModal() (modal) |
|---|---|---|
| Renders in top layer | No | Yes |
| Background inert | No | Yes |
| Escape closes | No | Yes |
| Light-dismiss on backdrop | No | No (opt-in) |
::backdrop renders |
No | Yes |
| Focus trapped inside | No | Yes |
Most UI that uses the word "modal" wants showModal(). Setting the open attribute in HTML produces a non-modal dialog. Modals specifically require the JavaScript call, because the modal behavior depends on the dialog being promoted to the top layer at runtime.
method="dialog" on a form inside the dialog
A form inside a dialog can use a special form method: dialog. When a form with method="dialog" is submitted:
- the form does not send a network request
- the dialog closes
- the dialog's
returnValueproperty becomes the value of the button that submitted the form - the
closeevent fires on the dialog
This turns a confirmation dialog into one element with two buttons and one event listener:
<dialog id="confirm">
<form method="dialog">
<p>Delete this item?</p>
<button value="cancel">Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<script>
const dlg = document.getElementById('confirm');
document.querySelector('#delete-btn').addEventListener('click', () => dlg.showModal());
dlg.addEventListener('close', () => {
if (dlg.returnValue === 'confirm') {
// perform the delete
}
});
</script>
One event handler for the whole dialog, regardless of how many options it offers. This is often a nice simplification over wiring separate click handlers for each button.
Styling the backdrop
The backdrop is a real pseudo-element that CSS can style:
dialog::backdrop {
background: rgb(0 0 0 / 0.6);
backdrop-filter: blur(4px);
}
dialog[open] {
animation: dialog-in 200ms ease-out;
}
@starting-style {
dialog[open] {
opacity: 0;
transform: scale(0.95);
}
}
The @starting-style rule, paired with transition-behavior: allow-discrete, animates the dialog from nothing as it enters the DOM. Because dialogs transition from display: none to display: block, which is a discrete change, standard CSS transitions on the dialog itself don't cover enter animations. The starting-style rule fills that gap.
Focus management details
When showModal() runs:
- Focus moves to the first focusable descendant of the dialog by default.
- The
autofocusattribute overrides the default. Inside a dialog,autofocusis a useful pattern, not an anti-pattern like it often is elsewhere. - Tab and Shift-Tab cycle within the dialog. The browser enforces this.
When the dialog closes:
- Focus returns to whichever element was focused when
showModal()was called. - If that element has been removed from the DOM, focus falls back to the body, the same as any case where a focused element disappears.
The focus-return behavior is tracked through the top-layer mechanism, which makes it more resilient to DOM changes than a JavaScript reference to the previously-focused element would be.
Light-dismiss (click-outside-to-close)
Not on by default. The common pattern is one line:
dialog.addEventListener('click', (event) => {
if (event.target === dialog) dialog.close();
});
This works because clicks on content inside the dialog bubble with event.target set to the inner element, while clicks on the backdrop area register with event.target as the dialog itself. The check distinguishes the two.
A newer closedby attribute is in active development that would make light-dismiss declarative. Worth checking current browser support before relying on it.
Invoker commands with <dialog>
The invoker commands API lets a button open a dialog declaratively, with no click handler on the trigger:
<button commandfor="confirm" command="show-modal">Delete item</button>
<dialog id="confirm">...</dialog>
Paired with <form method="dialog"> inside the dialog, a confirmation modal is possible with zero JavaScript on either the trigger or the buttons. The close event on the dialog is still the place to handle what the user chose. Invoker commands are newer than the dialog element itself, so current browser support is worth verifying before relying on them in production.
Non-modal dialog use cases
The non-modal variant, opened with show() instead of showModal(), is for dialogs that should not block interaction with the rest of the page:
- A pinned panel that the user can leave open while working elsewhere on the page.
- A non-blocking confirmation or notification, though a
role="status"live region is often better for passive announcements. - An inline explanation that appears near a form field.
The rule of thumb: if the user needs to keep interacting with the rest of the page while the dialog is visible, it's non-modal. If the dialog demands a decision before anything else happens, it's modal.
Composite patterns where <dialog> is the right element
- Confirmations. "Are you sure?" before a destructive action, built with
<form method="dialog">and acloseevent listener. - Form-in-modal. A modal that contains a form, where submission can either post to a server, using a real form action, or submit as a dialog to capture user input locally.
- Alerts. Critical messages that need acknowledgement before work continues. Modal, often with a single button.
- Media viewers. Lightbox-style image or video viewers. The top layer means the media renders above page content without z-index tuning.
- Settings panels. Either modal or non-modal depending on whether the rest of the page remains relevant.
Already HTML-available
Patterns that HTML handles natively, which are often rebuilt with libraries or custom code:
- Custom modal components using portals and stacking contexts. The top layer renders above all z-index without escaping the DOM tree.
- Scroll-lock behavior on the body while a modal is open. Background inertness is built in for modal dialogs.
- Focus trap implementations. Tab cycling within the dialog is built in.
- Escape-to-close listeners. Built in for modal dialogs.
- ARIA role and state management on custom dialog elements. The native element sets the correct role and modal state without ARIA attributes.
- Tracking the previously focused element to return focus on close. Built in through the top-layer mechanism.
Reference:
- WHATWG
- the-dialog-element
- caniuse.com
- mdn-html_elements_dialog
- MDN Docs
- Web/HTML/Element/dialog
- CSS: @starting-style
- CSS: ::backdrop
- WAI-ARIA APG
- Dialog (Modal) Pattern