Tutorial
How to make a modal accessible
In short
Use the native <dialog> element with showModal(): it traps focus inside the dialog, makes the background inert, closes on Escape and provides the dialog role. Give the dialog a name with aria-labelledby and return focus to the triggering button on close.
At a glance
Use the native <dialog>
Replace the CSS-toggled <div> with a <dialog> element and open it with showModal().
Name the dialog
Link the heading to the dialog with aria-labelledby so screen readers announce its purpose.
Verify Escape and the focus trap
showModal() keeps focus inside the dialog and closes on Escape. Test with the keyboard only.
Return focus
On the close event, move focus back to the button that opened the dialog.
A modal is one of the most common barriers on the web. Typical failures: the keyboard can tab into the background, Escape closes nothing, the screen reader never announces that a dialog is open, and after closing the focus lands nowhere. One element the browser already ships fixes all four.
The problem
This pattern looks like a dialog but is not one for assistive technology: no role, no name, no focus trap, no Escape.
<button onclick="openModal()">Newsletter abonnieren</button>
<div class="modal" id="modal" style="display: none">
<div class="modal-box">
<h2>Newsletter</h2>
<p>Melde dich fuer unseren Newsletter an.</p>
<input type="email" placeholder="E-Mail" />
<button onclick="closeModal()">Schliessen</button>
</div>
</div>- The background stays reachable with Tab (focus leaves the dialog).
- Escape does not close the dialog.
- A screen reader announces no dialog because role and name are missing.
- After closing, focus is lost.
The fix: the native <dialog> element
showModal() does most of the work: it traps focus inside the dialog (giving 2.1.2 No Keyboard Trap a defined exit), makes the rest of the page inert, shows a backdrop and closes on Escape. You only have to name the dialog and return focus on close.
<button type="button" id="open">Newsletter abonnieren</button>
<dialog id="modal" aria-labelledby="modal-title">
<h2 id="modal-title">Newsletter</h2>
<p>Melde dich fuer unseren Newsletter an.</p>
<label for="email">E-Mail</label>
<input id="email" type="email" />
<button type="button" id="close">Schliessen</button>
</dialog>const dialog = document.getElementById("modal");
const openBtn = document.getElementById("open");
openBtn.addEventListener("click", () => {
// showModal() traps focus inside the dialog, makes the rest of the page inert,
// and lets the Escape key close it. All three are WCAG requirements you no longer
// have to hand-roll.
dialog.showModal();
});
document.getElementById("close").addEventListener("click", () => dialog.close());
// Return focus to the trigger when the dialog closes (native or via Escape).
dialog.addEventListener("close", () => openBtn.focus());Test (keyboard only)
- Open the dialog with Enter/Space on the button.
- Tab through the dialog: focus must cycle inside it, never into the background.
- Escape must close the dialog.
- After closing, focus must be back on the triggering button.
If you need the ARIA pattern without <dialog> (e.g. for old browsers), you must reimplement the focus trap, Escape handling and background inertness in JavaScript yourself, plus role="dialog" and aria-modal="true". The native element removes exactly this error-prone part.
Related WCAG criteria
Frequently asked questions
- Do I need role="dialog" when I use <dialog>?
- No. The native <dialog> element already carries the dialog role. An extra role="dialog" is redundant. Only a hand-built dialog made of a <div> needs role="dialog" and aria-modal="true".
- Why showModal() and not show()?
- show() opens a non-modal dialog with no focus trap, no inert background and no Escape-to-close. For a real modal you need showModal().
- How do I return focus correctly?
- Remember the triggering element (or read document.activeElement on open) and call .focus() on it in the close event. Otherwise the screen reader jumps to the top of the page after closing.
Request a free scan
Tell us your domain — we run a free initial scan and show you the most important barriers.