HTML Dialog Element Guide
Complete reference for the HTML dialog element — modal and non-modal dialogs, ::backdrop, form[method='dialog'], events, and accessibility
You need a confirmation modal. You install a React modal library — 8 KB, plus you need to manage focus trapping, Escape key handling, scroll locking, a backdrop overlay, and ARIA attributes. Or you use the native <dialog> element: showModal() gives you all of that for free. The problem is the documentation is scattered — showModal() vs show(), ::backdrop styling, form method="dialog" for return values, close events, and focus management each live on different MDN pages.
Why This Guide (Not the HTML Details & Summary Reference)
PureDevTools has an HTML Details & Summary Reference for disclosure widgets (collapsible FAQ sections, accordions). This guide covers <dialog> — modal and non-modal dialogs that overlay the page. Both reduce JavaScript dependency, but they solve different problems: details/summary toggles inline content; dialogs trap focus, block interaction with the page behind them, and support form-based return values.
HTML Dialog Element Guide
The <dialog> element is the browser-native way to create modal and non-modal dialogs. It provides focus trapping, Escape key handling, a built-in backdrop, and correct ARIA semantics — all without a single line of manual JavaScript for the basics.
<!-- Minimal modal dialog -->
<dialog id="modal" aria-labelledby="modal-title">
<h2 id="modal-title">Confirm deletion</h2>
<p>This action cannot be undone.</p>
<form method="dialog">
<button value="cancel" autofocus>Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<button onclick="document.getElementById('modal').showModal()">Open</button>
<script>
document.getElementById('modal').addEventListener('close', (e) => {
if (e.target.returnValue === 'confirm') deleteItem();
});
</script>
Modal vs Non-Modal Dialogs
The dialog element supports two modes, opened by different JavaScript methods.
showModal() — Modal dialog
dialog.showModal() opens the dialog in modal mode:
- Moves the element to the top layer (above everything, including fixed position elements)
- Renders the
::backdroppseudo-element behind the dialog - Traps focus — keyboard navigation cannot leave the dialog
- Pressing Escape fires a
cancelevent, then closes the dialog - All content outside the dialog becomes inert (keyboard-inaccessible)
const dialog = document.getElementById('my-dialog');
dialog.showModal(); // true modal — use this for confirmations, forms, alerts
show() — Non-modal dialog
dialog.show() opens the dialog inline without blocking the rest of the page:
- Does not move to the top layer
- Does not render the
::backdrop - Does not trap focus
- Does not close on Escape by default
dialog.show(); // non-modal — use for contextual panels, toasts, or floating info
Closing a Dialog
dialog.close(returnValue?)
dialog.close(); // Close with empty returnValue
dialog.close('confirmed'); // Close with a specific return value
dialog.close('cancelled');
The returnValue string is available as dialog.returnValue in the close event handler. Use it to determine what action the user took.
form[method=“dialog”]
The cleanest way to close a dialog is with a form using method="dialog". No JavaScript submit handler needed:
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="save">Save</button>
</form>
When the user clicks a button, the form closes the dialog and sets dialog.returnValue to the button’s value attribute. Native form validation still runs before the dialog closes.
Listening for the Close Event
const dialog = document.getElementById('dialog');
dialog.addEventListener('close', () => {
switch (dialog.returnValue) {
case 'save': saveData(); break;
case 'delete': deleteItem(); break;
default: break; // Cancelled or Escape key
}
});
The close event fires for all close paths: programmatic close(), Escape key, and form[method="dialog"] submission.
::backdrop Styling
The ::backdrop pseudo-element is the overlay behind a modal dialog. Customize it with any CSS:
/* Semi-transparent dark overlay (browser default is similar) */
dialog::backdrop {
background-color: rgb(0 0 0 / 50%);
}
/* Frosted glass */
dialog::backdrop {
background-color: rgb(0 0 0 / 40%);
backdrop-filter: blur(4px);
}
The backdrop only appears for modal dialogs opened with showModal(). It fills the entire viewport and sits in the top layer between the page content and the dialog.
Dialog Animations with CSS
Modern CSS (Chrome 117+, Firefox 129+, Safari 17.5+) supports animating dialogs in and out using @starting-style and allow-discrete transitions:
dialog {
opacity: 0;
transform: scale(0.95);
transition:
opacity 0.2s ease,
transform 0.2s ease,
overlay 0.2s allow-discrete,
display 0.2s allow-discrete;
}
dialog[open] {
opacity: 1;
transform: none;
}
/* Animate FROM this state when opening */
@starting-style {
dialog[open] {
opacity: 0;
transform: scale(0.95);
}
}
/* Animate the backdrop too */
dialog::backdrop {
background: rgb(0 0 0 / 0%);
transition: background 0.2s ease, overlay 0.2s allow-discrete, display 0.2s allow-discrete;
}
dialog[open]::backdrop {
background: rgb(0 0 0 / 50%);
}
@starting-style {
dialog[open]::backdrop {
background: rgb(0 0 0 / 0%);
}
}
The overlay and display properties in the transition list are required for exit animations — they tell the browser to keep the element visible during the transition even as it leaves the open state.
Accessibility Requirements
1. Provide an accessible name
<!-- Use aria-labelledby with a visible heading (preferred) -->
<dialog aria-labelledby="dlg-title">
<h2 id="dlg-title">Settings</h2>
...
</dialog>
<!-- Or aria-label when there's no visible heading -->
<dialog aria-label="Photo preview">
...
</dialog>
Screen readers announce the dialog name when focus enters. Without it, they only say “dialog.”
2. Manage focus with autofocus
<dialog>
<!-- Focus the primary action for informational dialogs -->
<button autofocus>Close</button>
<!-- Focus Cancel for destructive confirmations -->
<button value="cancel" autofocus>Cancel</button>
<button value="delete">Delete</button>
</dialog>
3. Return focus to the opener
The browser returns focus to the opener element when the dialog closes. If the opener may not be available, store it and restore focus manually:
let opener = null;
dialog.addEventListener('close', () => {
opener?.focus();
opener = null;
});
function openDialog(triggerEl) {
opener = triggerEl;
dialog.showModal();
}
4. Handle Escape gracefully
Only block Escape (cancel event) when there are unsaved changes — and always offer an alternative close path:
dialog.addEventListener('cancel', (event) => {
if (hasUnsavedChanges()) {
event.preventDefault();
showDiscardConfirmation().then(ok => { if (ok) dialog.close(); });
}
});
Blocking Escape unconditionally violates WCAG 2.1.2 (No Keyboard Trap).
Browser Support
The <dialog> element is Baseline 2022 — supported in all modern browsers:
| Browser | Since |
|---|---|
| Chrome | 37 (modal in 37, stable in 43) |
| Firefox | 98 |
| Safari | 15.4 |
| Edge | 79 |
For older browsers (IE 11, pre-2022 Safari), use a polyfill like dialog-polyfill by GoogleChrome.
Common Mistakes
Using the open attribute instead of showModal(): Adding open via HTML or setAttribute() creates a non-modal dialog with no backdrop and no focus trap.
Missing aria-labelledby: Screen reader users hear “dialog” without context. Always label the dialog with its visible heading.
Manual focus trap code: showModal() provides a native, robust focus trap. Manual implementations are error-prone and often conflict with the browser’s native behavior.
Not using form[method="dialog"]: Using global variables or multiple event listeners to track dialog outcome instead of the built-in returnValue mechanism.
Suppressing Escape unconditionally: Blocking Escape without an alternative close mechanism traps keyboard users and violates WCAG 2.1.2.
FAQ
What is the difference between showModal() and show()?
showModal() opens the dialog as a modal — it moves to the top layer, shows a backdrop, traps focus, and blocks the rest of the page. show() opens the dialog inline (non-modal) — no backdrop, no focus trap, the rest of the page remains interactive. Use showModal() for confirmations, forms, and any dialog that requires the user’s attention before continuing.
How do I get the result from a dialog?
Use form[method="dialog"] with buttons that have value attributes. When a button is clicked, the dialog closes and dialog.returnValue is set to that button’s value. Read it in the close event handler. Alternatively, call dialog.close('your-value') manually and read dialog.returnValue in the close handler.
How do I close a modal when clicking outside?
Click events on the backdrop fire on the <dialog> element. Check if the click coordinates are outside the dialog’s bounding box using getBoundingClientRect(), then call dialog.close() if they are.
Does the dialog element work without JavaScript?
You can display a non-modal dialog without JavaScript by including the open attribute in HTML. However, for proper modal behavior (showModal, close, returnValue), JavaScript is required.
How do I animate the dialog opening and closing?
Use CSS transitions with overlay allow-discrete and display allow-discrete in the transition list, combined with @starting-style to define the initial animation state. This approach works in Chrome 117+, Firefox 129+, and Safari 17.5+. For older browsers, use JavaScript to add/remove CSS classes to trigger animations.
Is the dialog element accessible?
Yes — showModal() provides ARIA dialog role semantics, automatic focus management, and a native focus trap. You still need to add aria-labelledby (or aria-label) for the accessible name, and ensure proper autofocus placement. Screen reader support for <dialog> is robust in all major screen readers paired with supported browsers.