Back to Learn Hub
Comprehensive Guide

Accessible Modal Dialogs

Focus trapping, keyboard controls, and proper announcements

Modal Dialog Fundamentals

Modal dialogs interrupt the user's workflow. The key ARIA attributes are role='dialog', aria-modal='true', and aria-labelledby to connect the dialog to its title.

role='dialog'

Tells screen readers this is a dialog requiring user attention before returning to the main content.

aria-modal='true'

Indicates the dialog is modal—content behind it is inert and shouldn't be accessible.

aria-labelledby

Points to the dialog's title element. Users hear "Dialog: [title]" when it opens.

Interactive Demo

Click the button to open an accessible modal. Try pressing Escape, clicking outside, or using Tab.

Don't: Generic Div

<!-- ❌ INACCESSIBLE -->
<div class="modal">
  <div class="modal-content">
    <span onclick="close()">&times;</span>
    <h2>My Modal</h2>
  </div>
</div>

Do: Semantic Dialog

<!-- ✅ ACCESSIBLE -->
<div role="dialog" 
     aria-modal="true" 
     aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm</h2>
  <button aria-label="Close">×</button>
</div>
Use Native <dialog> When Possible
The HTML <dialog> element with showModal() provides focus trapping and Escape handling automatically.

A modal dialog is a window layered over the page that takes over interaction until it is dismissed. While it is open, everything behind it should be inert: not focusable, not readable by a screen reader, and not scrollable. That takeover is exactly what makes the pattern risky. Done well it is a short, predictable interruption with an obvious exit. Done badly it is either a trap that keyboard users cannot leave or, more often, a layer that keyboard and screen reader users never reach because focus stayed on the page beneath it.

The demo above implements the pattern by hand so each part is visible: a container with role="dialog" and aria-modal="true", a heading referenced by aria-labelledby, focus moved into the container on open, Tab and Shift+Tab wrapped inside it, Escape and a labelled close button to dismiss, scrolling locked on the body, and focus returned to the button that opened it. The native <dialog> element with showModal() gives you most of this for free and is the better choice in new code.

WCAG 2.2 Success Criteria Involved

Success criterionLevelWhat it requires here
2.1.1 KeyboardAThe dialog can be opened, operated, and closed with the keyboard alone.
2.1.2 No Keyboard TrapAHolding focus inside the dialog is acceptable only because Escape and the close button let the user leave; without an exit, the trap is a failure.
2.4.3 Focus OrderAFocus moves into the dialog when it opens and back to the trigger when it closes, so the sequence stays meaningful.
2.4.7 Focus VisibleAAEvery control inside the dialog, including the icon-only close button, shows a visible focus indicator.
4.1.2 Name, Role, ValueAThe dialog exposes its role, its modal state, and an accessible name taken from its heading.
1.3.1 Info and RelationshipsAThe title is a real heading and is programmatically associated with the dialog, not just placed near it.
3.2.1 On FocusAA dialog must not open merely because an element received focus; opening is a change of context and needs an explicit action.

Expected Keyboard Interaction

Keyboard interaction for a modal dialog
KeyExpected result
Enter or SpaceOn the trigger button: opens the dialog and moves focus inside it.
TabMoves to the next focusable control inside the dialog; from the last control it wraps to the first.
Shift + TabMoves to the previous control; from the first it wraps to the last.
EscapeCloses the dialog and returns focus to the element that opened it.
Enter or SpaceOn Confirm, Cancel, or Close: activates that control; Cancel and Close also return focus to the trigger.

The Failures We See Most Often

  1. Focus never enters the dialog. The overlay is shown with CSS while focus stays on the page behind it, so a keyboard user keeps tabbing through content they cannot see and a screen reader never announces that anything opened.
  2. There is no keyboard exit. No Escape handler, and a close control that is an icon in a span with no name. Mouse users click the backdrop; everyone else is stuck.
  3. Focus is not restored on close. Closing the dialog drops focus to the top of the document. The user loses their place and has to tab back through the whole page to continue.

The accessible dialog and modal guide goes further: the native <dialog> element and showModal(), where initial focus should land for confirmation dialogs versus forms, what to do when the trigger has been removed from the page, and when alertdialog is the right role.