The <dialog> Element
<div> positioned with CSS, plus hand-written JavaScript to trap focus, handle Escape, and manage the backdrop. <dialog> builds all of that into the browser: a native, accessible modal (or non-modal) window with almost no custom code required.Basic Markup
dialog-basic.html
<dialog id="welcome-dialog">
<h2>Welcome!</h2>
<p>Thanks for signing up. Let's get you started.</p>
<button id="close-btn">Got it</button>
</dialog>
<button id="open-btn">Open dialog</button>
<script>
const dialog = document.getElementById('welcome-dialog');
document.getElementById('open-btn').addEventListener('click', () => {
dialog.showModal();
});
document.getElementById('close-btn').addEventListener('click', () => {
dialog.close();
});
</script><dialog> renders invisible by default (like display: none) until it's opened with showModal() or show().showModal() vs show()
Method | Behavior |
|---|---|
showModal() | Opens as a true modal: traps focus inside, dims the rest of the page with a backdrop, disables interaction outside the dialog, and closes on Escape |
show() | Opens as a non-modal dialog: no backdrop, no focus trap, the rest of the page stays interactive |
showModal(). Reach for show() only for lightweight, non-blocking panels (like a "tips" popover) where the user should still be able to interact with the rest of the page.The ::backdrop Pseudo-Element
showModal(), the browser renders a ::backdrop layer behind the dialog, covering the whole viewport. You can style it directly with CSS.backdrop.html
<style>
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(2px);
}
</style>
<dialog id="confirm-dialog">
<p>Delete this item permanently?</p>
<button id="cancel-btn">Cancel</button>
<button id="delete-btn">Delete</button>
</dialog>Closing With a method="dialog" Form
<form> with method="dialog", submitting that form automatically closes the dialog—no JavaScript required for the close action itself. The button that triggered submission is recorded as the dialog's returnValue.form-dialog.html
<dialog id="confirm-dialog">
<form method="dialog">
<p>Delete this item permanently?</p>
<button value="cancel">Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<script>
const dialog = document.getElementById('confirm-dialog');
dialog.addEventListener('close', () => {
if (dialog.returnValue === 'confirm') {
console.log('User confirmed deletion');
}
});
</script>returnValue tells you which button was pressed.Closing on Backdrop Click
<dialog> doesn't close on backdrop click by default. A common pattern checks whether the click target was the dialog itself (the backdrop area) rather than its content.backdrop-click.html
<script>
dialog.addEventListener('click', (event) => {
if (event.target === dialog) {
dialog.close();
}
});
</script>Why Native Beats a Div-Based Modal
<div> modals require you to manually implement focus trapping (keeping Tab from escaping the modal), Escape-to-close, backdrop dimming, and correct ARIA roles. Get any of these wrong and keyboard or screen reader users get stuck or lost. <dialog> with showModal() handles all of it correctly out of the box.showModal() is almost always the right call. show() is meant for non-blocking, dismissible panels only.<dialog> is positioned by the browser (centered, fixed) by default, but you can override its position, size, and appearance with ordinary CSS just like any other element.Focus Management on Open
showModal() runs, the browser automatically moves focus to the first focusable element inside the dialog (or the dialog itself if nothing inside is focusable). When the dialog closes, focus automatically returns to whatever element opened it—no manual bookkeeping required.focus-flow.html
<button id="open-btn">Delete account</button>
<dialog id="confirm-dialog">
<form method="dialog">
<p>This action cannot be undone. Continue?</p>
<button value="cancel" autofocus>Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<script>
document.getElementById('open-btn').addEventListener('click', () => {
document.getElementById('confirm-dialog').showModal();
// Focus already lands on the "Cancel" button via autofocus.
// Closing the dialog returns focus to "Delete account" automatically.
});
</script>autofocus to the safer default action (here, "Cancel") is a good practice—it means pressing Enter immediately after the dialog opens does the safe thing, not the destructive one.Dialog as a Non-Modal Notification
non-modal-toast.html
<dialog id="toast" style="position: fixed; bottom: 24px; right: 24px;">
<p>Saved successfully.</p>
</dialog>
<script>
const toast = document.getElementById('toast');
toast.show();
setTimeout(() => toast.close(), 3000);
</script>show() doesn't trap focus or dim the page, it's appropriate here—the user should be able to keep working while a transient confirmation message appears and disappears on its own.Progressive Enhancement Note
<dialog> has broad support in current browsers, but if you must support very old browsers, feature-detect before relying on it.feature-detect.html
<script>
if (typeof HTMLDialogElement === 'function') {
// Safe to use dialog.showModal()
} else {
// Fall back to a hand-built modal implementation
}
</script>showModal() opens a true modal: focus trap, backdrop, Escape-to-close, disabled background interaction.
show() opens a non-modal panel with none of those behaviors.
Style the modal backdrop with the ::backdrop pseudo-element.
A nested <form method="dialog"> closes the dialog on submit and records which button was pressed via returnValue.
Native <dialog> avoids the focus-trapping and ARIA bugs common in hand-built div-based modals.