What a modal owes
Miss the first and a screen reader user never hears the dialog. Miss the second and they wander into the page behind it, reading things they cannot see. Miss the last and they are returned to the top of the document.
- Focus goes in when it opens — to the dialog itself, or to the first control in it.
- Focus stays in while it is open: Tab from the last control returns to the first. Everything behind it is inert.
- Escape closes it without saving.
- Focus comes back to whatever opened it, so the user is where they left off.
- It has a name, and announces itself as a dialog.
<dialog> does almost all of it
showModal() gives you the focus trap, the inert background, the Escape key and the backdrop, all from the platform. show() — without "modal" — does none of that and is for a non-modal popover. The only thing left to you is returning focus on close.
<dialog id="quote" aria-labelledby="quote-title">
<h2 id="quote-title">Ask for a quote</h2>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="send">Send</button>
</form>
</dialog>
<script>
const dialog = document.querySelector("#quote");
opener.addEventListener("click", () => dialog.showModal()); // focus in, background inert, Escape works
dialog.addEventListener("close", () => opener.focus()); // the one part you still write
</script>
A <dialog> opened with showModal() has role="dialog" and modal semantics. Adding aria-modal="true" by hand is only needed when you are building one out of a <div> — and then you also owe the trap, the inert background and the Escape handler.
If you must build it by hand
inert is the modern way to make everything behind the dialog unreachable — it removes the subtree from the tab order and from the accessibility tree in one attribute, which is exactly the "background is not there" behaviour a modal needs.
let lastFocused = null;
function open() {
lastFocused = document.activeElement;
panel.hidden = false;
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-modal", "true");
document.querySelector("main").setAttribute("inert", ""); // everything behind it
panel.querySelector("h2").setAttribute("tabindex", "-1");
panel.querySelector("h2").focus();
}
function close() {
panel.hidden = true;
document.querySelector("main").removeAttribute("inert");
lastFocused?.focus();
}
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !panel.hidden) close(); });
Things that are not dialogs
- A cookie banner that blocks the page is a modal, whether you called it one or not — it needs all of the above.
- A toast is not a dialog: it does not take focus. It is a live region, which is the next lesson.
- A menu or a popover is not modal: Escape should close it and focus should return, but the page behind stays usable.
- A full-screen loading overlay should not trap focus; mark it
aria-busyand leave the page alone.