Modals are one of the most common places accessibility quietly breaks. A keyboard user gets stranded behind the overlay, a screen reader keeps reading the page underneath, or pressing Escape does nothing. The good news is that an accessible modal dialog is far easier to build correctly today than it was a few years ago, thanks to the native HTML dialog element.
This guide walks through what a robust modal actually needs: the dialog element and showModal(), trapping focus, restoring focus when the modal closes, wiring up Escape, the right ARIA, and the mistakes that show up most often in real audits.
Start with the native dialog element
Before reaching for a library or a custom <div>, use the platform. The <dialog> element opened with the showModal() method gives you most modal behavior for free, and it is supported across all current major browsers.
Calling dialog.showModal() does three things that are genuinely hard to replicate by hand: it renders the dialog in the browser's top layer (so it sits above everything without z-index fights), it makes the rest of the document inert (background content is not focusable or clickable), and it exposes the element as a modal to assistive technology. Closing with dialog.close() or the form pattern below tears all of that down cleanly.
A minimal, correct structure looks like this:
<dialog id="confirm" aria-labelledby="confirm-title">— the container, named by its heading<h2 id="confirm-title">Delete project?</h2>— the visible title that names the dialog<form method="dialog">with a<button value="cancel">and<button value="confirm">— submitting closes the dialog and records which button was used viadialog.returnValue
The key distinction: showModal() creates a true modal. The older show() method (and the open attribute) creates a non-modal dialog that does not trap focus or make the background inert, so it is not what you want for a blocking confirmation or a form overlay.
Focus trapping and restoring focus
Trapping focus inside the modal
While a modal is open, Tab and Shift+Tab must cycle only through the controls inside it. With showModal() the browser handles this automatically because everything outside the top layer is inert. This is the single biggest reason to prefer the native element: hand-rolled focus traps are notoriously buggy, especially around iframes, shadow DOM, and dynamically added content.
If you cannot use <dialog> and must build on a <div>, you have two options. The cleaner one is to set the inert attribute on every sibling of the modal (or on the main app wrapper), which removes that content from focus and the accessibility tree in one move. The harder one is a JavaScript trap: query all focusable elements, listen for Tab on the first and last, and loop focus manually. Reach for inert first.
Restoring focus on close
This step is skipped constantly, and it badly hurts keyboard users. When the modal closes, focus must return to the element that opened it, usually the trigger button. Otherwise focus falls back to the top of the document and the user has to tab through the whole page again to find their place.
Capture the trigger before opening and restore it after closing: const opener = document.activeElement; then, in a close event listener, opener.focus();. Native <dialog> does not do this for you, so it is one of the few pieces of glue code you still must write. Predictable focus order is required by WCAG 2.4.3 Focus Order (Level A), and it pairs directly with a visible focus indicator (2.4.7 Focus Visible, AA) so users can see where they have landed.
Escape to close and modal semantics
Escape closes the dialog
Users expect Escape to dismiss a modal, and a keyboard route out is part of meeting WCAG 2.1.1 Keyboard (Level A). With showModal() this is built in: pressing Escape fires a cancel event and then closes the dialog, no key handling required. If you build a custom modal, you must add a keydown listener for the Escape key yourself, and make sure it does not also close a parent menu or trigger unrelated shortcuts.
A common refinement: for forms with unsaved changes, listen for the cancel event and call event.preventDefault() to confirm before discarding. Just be sure there is always a way out, do not trap users in a modal they cannot leave.
Get the ARIA naming right
When you use showModal(), the browser already conveys modal semantics, so you do not manually add role="dialog" or aria-modal="true"; doing so can double up announcements. What you do still owe is an accessible name. Point aria-labelledby at the visible heading, or use aria-label if there is no visible title. Add aria-describedby when a body paragraph should be read as part of the dialog's introduction.
If you are stuck on a custom <div> implementation, then you do need the full set: role="dialog", aria-modal="true", and a name via aria-labelledby or aria-label. Native <dialog> is almost always the safer choice.
Common mistakes to avoid
- Hiding the background with CSS only. Setting
opacityordisplayon an overlay does not stop Tab from reaching links behind it. Use<dialog>withshowModal(), orinert. - Forgetting to restore focus. After close, focus must return to the trigger, not jump to the page top.
- Using a non-semantic close control. An
×glyph in a<span>is invisible to screen readers and not keyboard-focusable. Use a real<button>with an accessible name like "Close dialog". - Tiny tap targets. The close button and any actions should meet WCAG 2.2's 24x24 CSS pixel minimum target size (2.5.8 Target Size Minimum, AA), with enough spacing between them.
- Low-contrast overlays and buttons. Dialog text needs 4.5:1 (1.4.3), and buttons, icons, and the focus outline need 3:1 against their background (1.4.11). Verify with our contrast checker.
- Auto-focusing a destructive action. Do not put initial focus on Delete or Confirm, an accidental Enter is destructive. Focus the title, the first field, or the safe default.
- Letting a focused element get hidden under sticky UI. WCAG 2.2's 2.4.11 Focus Not Obscured (Minimum, AA) means the focused control must stay at least partly visible.
If you maintain a component library, dialogs are worth auditing once and reusing everywhere. You can scan any page for these issues, missing names, focus problems, contrast failures, with the free checker at AccessScan, which flags the most common modal regressions automatically. For broader coverage, the accessibility checklist walks through related interactive patterns.
Why this matters for compliance
Modal accessibility is not just polish. Keyboard operability (2.1.1, Level A), focus order (2.4.3, Level A), a visible focus indicator (2.4.7, AA), and focus not being obscured (2.4.11, AA) are all real success criteria. Under the European Accessibility Act, whose requirements apply from 28 June 2025, the baseline is WCAG 2.2 Level A and AA via EN 301 549. A modal that traps keyboard users or hides focus is a concrete failure against that baseline.
The same criteria underpin ADA-related expectations in the US, where WCAG 2.1/2.2 Level AA is the de-facto standard used in settlements and guidance, so getting dialogs right pays off across jurisdictions. Building on the native <dialog> element, restoring focus, and respecting Escape gets you most of the way to a compliant, genuinely usable accessible modal dialog.