Wasn’t it great — nay, amazing — the day we got a native HTML <dialog> element? It’s nearly ten years old now. Even so, I always find myself looking up how it works, when to use it, ideas for styling it, and whatnot. There’s a lot of nuance to this seemingly little piece of web architecture, and it’s high time I give it a proper look — for future me, but maybe for you as well.
Marking Up a <dialog>
This is the basic markup:
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog">...</dialog>
It won’t open by default. We could manually set the open attribute:
<dialog id="dialog" open>...</dialog>
But when do you ever want to open a dialog by default? It’s a rare use case. Instead, we have a JavaScript show() method for that. We can set up variables for the dialog and button, then invoke the method:
const dialogButton = document.querySelector('#dialog-button');
const dialog = document.querySelector('#dialog');
dialogButton.addEventListener('click', () => {
dialog.show();
})
That works, but buyer beware — that treats the dialog as more of a pop-up than a modal, and modal is what you’ll want in most cases. The key differences? A modal includes a backdrop, is automatically positioned in the center of the page, and allows the Esc key to close it.
So, maybe invoke the showModal() method instead:
const dialogButton = document.querySelector('#dialog-button');
const formDialog = document.querySelector('#dialog');
dialogButton.addEventListener('click', () => {
formDialog.showModal();
})
It opens, positions, and responds to Esc to close.
More About Closing
Hitting the Esc key when the dialog is in focus (and notice how it’s in focus by default) will close the dialog. But if you want UI that closes the dialog — like a button — you can add that inside the element:
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog">
<button id="dialog-close">Close</button>
<!-- etc. -->
</dialog>
That doesn’t work right out of the box, but there’s a close() method for it:
const formButton = document.querySelector('#dialog-button');
const formDialog = document.querySelector('#dialog');
const formClose = document.querySelector('#dialog-close');
formButton.addEventListener('click', () => {
formDialog.showModal();
})
formClose.addEventListener('click', () => {
formDialog.close();
})
There isn’t a corresponding closeModal() method, but close() works regardless of how the dialog was opened.
If you want a JavaScript-less approach, you can do it declaratively in HTML:
<dialog id="dialog">
<form method="dialog">
<button type="submit">Close dialog</button>
</form>
</dialog>
It’s possible to close the dialog without JavaScript using this pattern, though it’s worth considering how it may affect semantics.
Invoker Commands
As long as we’re talking about declarative closing, it’s worth mentioning an evolving feature called invoker commands designed specifically to open and close dialogs declaratively. It’s experimental, but here’s what it looks like:
<button command="show-modal" commandfor="my-dialog">Show Dialog</button>
<dialog id="my-dialog">...</dialog>
The command and commandfor attributes hook up the button directly to the dialog in HTML, invoking a specific dialog (#my-dialog) to show-modal. The same approach works for a closing button:
<dialog id="my-dialog">
<!-- Close #my-dialog -->
<button command="close" commandfor="my-dialog">Close Dialog</button>
</dialog>
And as Danny explains, you can hook all that up with JavaScript if you need to listen to those commands and fire off an event when they happen:
// Select all dialogs
const dialogs = document.querySelectorAll("dialog");
// Loop all dialogs
dialogs.forEach(dialog => {
// Listen for close (as normal)
dialog.addEventListener("close", () => {
// Dialog was closed
});
// Listen for command
dialog.addEventListener("command", event => {
// If command is show-modal
if (event.command == "show-modal") {
// Dialog was shown (modally)
}
// Another way to listen for close
else if (event.command == "close") {
// Dialog was closed
}
});
});
Keep an eye on browser support for invoker commands as it continues to roll out.
About Button Labeling
You might be tempted to use an “X” (or an SVG icon) for the close button’s label:
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog">
<button id="dialog-close">X</button>
</dialog>
That’s not ideal for screen readers. You still want the button to announce “Close Dialog” or something to that effect. If you’re keen on using an “X” icon, add a <span> containing the text you want read and visually hide it while preventing the icon from being announced using the aria-hidden attribute:
<button id="form-button">Open Dialog</button>
<dialog id="form-dialog">
<button id="form-close">
<span class="visually-hidden">Close modal</span>
<span aria-hidden="true"></span>
</button>
</dialog>
One more accessibility note: the close button gets focus when the dialog opens. You may or may not want that, because the button could unexpectedly close the dialog if the Space key is accidentally hit. Closing a modal isn’t exactly a destructive action and it can be reopened, but if you have other focusable elements in the dialog — a link or a form field, for example — consider giving one of those initial focus using the tabindex attribute.
Innate Inertness
Before getting into styling the backdrop, it’s worth noting that the page behind an open dialog is inert. Any sort of interaction — text selection, button clicking, focus, inputs, and so on — is unavailable while the dialog is open. That’s probably what you want, so it’s good that it isn’t something you need to configure manually. You just won’t see the inert attribute reflected in the markup when it happens.
This only applies when the dialog is set up as a modal. Using the show() method opens something more like a popover (think tooltips) rather than a true attention-hoarding modal that traps focus and sits on the very top layer.
You might wonder about competing dialogs — say, a popover and a modal open at the same time. You’d have to open the dialog popover first, since it does not trigger inert behavior. Only the modal dialog does. When the modal dialog is open, the popover dialog is not in the top layer and becomes inaccessible.
Styling the Backdrop
The backdrop is hard to even see in its default state. When a dialog is open, the page background is only very slightly tinted.

We can style the backdrop ourselves with the ::backdrop pseudo-element. Going full solid color is one option, but a little transparency — perhaps combined with a blur() filter — tends to work better since it preserves some visual context of the page behind it. The right approach depends on your design, but giving the backdrop a semi-transparent dark overlay with a subtle blur is a good starting point that keeps the dialog prominent without completely hiding the content beneath it.