What is a modal dialog?
A modal dialog is a window that appears on top of the main page and demands the user's attention before they can continue. While it's open, the background content is inactive, the user must respond to the dialog before doing anything else.
Modal dialogs are everywhere: confirmation prompts, forms, alerts, cookie notices, image lightboxes. When implemented incorrectly they create significant barriers for people using keyboard navigation or screen readers, the most common failure being that keyboard focus either doesn't move into the dialog, or leaks out behind it into content the user cannot see.
The three rules of dialog focus management
Correct focus management is mandatory under WCAG 2.2 SC 2.4.3 (Focus Order) and SC 2.1.1 (Keyboard). Every accessible dialog must follow three rules:
Move focus in
When a dialog opens, keyboard focus must move to the first focusable element inside it, or to the dialog container itself if there is no immediately actionable element.
Trap focus inside
While the dialog is open, Tab and Shift+Tab must cycle through elements within the dialog only. Focus must not leak out to the page behind it.
Return focus on close
When the dialog closes, however it closes, focus must return to the element that triggered it, so keyboard users do not lose their place on the page.
Demo 1: Native <dialog> element
The native HTML <dialog> element with showModal() is the simplest and most robust approach. The browser handles the focus trap automatically, and ::backdrop provides the overlay. No extra JavaScript focus management is needed.
<dialog> element with showModal() is supported in all modern browsers (Chrome, Firefox, Safari, Edge) and handles focus containment natively. Use it unless you have a specific reason not to.
A confirmation dialog triggered by a button. Try opening it and navigating entirely by keyboard.
Demo 2: ARIA dialog with custom focus trap
For richer interactions, or when you need to support older browser environments, you can build a dialog using role="dialog" and aria-modal="true" with a JavaScript focus trap. This pattern gives you full control over behaviour and styling.
A form dialog built with role="dialog". Tab cycles through the form fields and buttons. Fields are validated on submit.
Code examples
The following examples show the implementation for Demo 1 (native <dialog>). For most projects this is all you need.
<!-- Trigger button -->
<button
aria-haspopup="dialog"
id="openBtn"
onclick="openDialog()">
Delete file
</button>
<!-- Dialog -->
<dialog
id="confirmDialog"
aria-labelledby="dialogTitle"
aria-describedby="dialogDesc">
<h2 id="dialogTitle">Delete this file?</h2>
<p id="dialogDesc">This action cannot be undone.</p>
<div class="dialog-actions">
<button
id="cancelBtn"
onclick="closeDialog()">Cancel</button>
<button
onclick="confirmDelete()">Yes, delete</button>
</div>
</dialog>
const dialog = document.getElementById('confirmDialog');
const openBtn = document.getElementById('openBtn');
const cancelBtn = document.getElementById('cancelBtn');
// Open: showModal() traps focus natively
function openDialog() {
dialog.showModal();
cancelBtn.focus(); // move focus to first button
}
// Close: return focus to the trigger
function closeDialog() {
dialog.close();
openBtn.focus();
}
// Esc key, dialog handles this natively,
// but we restore focus manually on close.
dialog.addEventListener('close', () => {
openBtn.focus();
});
// Confirm action
function confirmDelete() {
// ... do the delete ...
closeDialog();
}
/* Native dialog element */
dialog {
border: none;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.25);
padding: 28px;
max-width: 440px;
width: calc(100% - 32px);
}
/* Semi-transparent backdrop */
dialog::backdrop {
background: rgba(15, 23, 42, 0.55);
backdrop-filter: blur(2px);
}
/* Ensure visible focus indicators inside */
dialog :focus-visible {
outline: 2px solid #246FC6;
outline-offset: 2px;
border-radius: 4px;
}
/* Button row */
.dialog-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 24px;
}
WCAG 2.2 success criteria
Accessible modal dialogs must meet the following Success Criteria. Failures on any of these are among the most common barriers audited by ExceedAbility in enterprise digital products.
All functionality via keyboard
Every action in the dialog, opening, closing, cancelling, confirming, must be operable without a mouse. No keyboard traps that a user cannot escape.
Focus containment with an exit
Focus must be contained inside the dialog while open (not leak to background content), but the user must always be able to close it, typically via Esc or a visible Close button.
Logical, sequential focus movement
When the dialog opens, focus must move into it. When it closes, focus must return to the trigger element. Focus must not jump unexpectedly or disappear.
Semantic role and naming
The dialog container must have role="dialog" (or use <dialog>) and an accessible name via aria-labelledby pointing to the dialog's heading.
Structure conveyed to assistive tech
The dialog's heading, descriptive text, form labels, and error messages must be programmatically determinable, not only conveyed through visual position or colour.
Keyboard focus is always visible
Every focusable element inside the dialog must have a visible focus indicator. Do not suppress :focus-visible outlines, a user must always know where focus is.
Common failures to avoid
- Focus does not move into the dialog on open, the most frequent failure. Always call
.focus()on the first element or useshowModal()which handles this. - Background content is still reachable by Tab, occurs when a custom overlay is used without a focus trap or
inertattribute on background content. - ESC key does not close the dialog, native
<dialog>handles this automatically. Custom dialogs need an explicitkeydownlistener for theEscapekey. - Focus is not returned to the trigger on close, causes keyboard users to lose their place. Always store a reference to the trigger before opening and call
.focus()on it when closing. - No accessible name on the dialog, without
aria-labelledbypointing to the heading, screen readers announce "dialog" with no context. - Error messages not associated with fields, use
aria-describedbyto link inputs to their error message elements, andaria-invalid="true"on fields that fail validation.
Want expert eyes on your dialogs?
ExceedAbility audits interactive components including modal dialogs, menus, and carousels, with assistive technology testing using real screen readers.
Contact ExceedAbility