Dialog
UI pattern
dialog role and a name, makes the page behind it inert, keeps focus inside until it closes, and returns focus to its launcher.A modal dialog is a window overlaid on the page that users have to deal with before they can return to anything else. While it's open, everything outside it is inert: users can't operate it, and assistive technology doesn't present it. Sighted users usually see the page behind it dimmed or obscured.
A modal dialog is a container rather than a control. It holds a title that names it, the content and controls for its task, and usually a button that closes it. A launcher, most often a button, opens it, and when it closes, focus goes back to the launcher or on to the next step of the task.
A button launcher meets every requirement of a button, and this entry's launcher criteria cover them as they apply to opening a modal dialog. Activating it moves focus into the dialog, which identifies itself as it opens, so the launcher needs no aria-expanded. An optional aria-haspopup="dialog" can also convey, before users activate it, that it opens a dialog.
There are two ways to build one. The native construction is the HTML <dialog> element opened with its showModal() method, which makes the rest of the page inert, moves focus into the dialog, and closes it when users press Escape. The custom construction is a generic element with the dialog role and aria-modal="true", and script has to supply its behaviors.
A non-modal dialog leaves the rest of the page available, so users can move between the two. An alert dialog is a modal dialog with the alertdialog role, reserved for a brief, important message that needs a response, such as confirming an action that can't be undone.
Other buttons that reveal content work differently. A menu button also opens content over the page, but the content is a menu of commands, and the button reports whether the menu is open with aria-expanded. A disclosure reveals content on demand, but it never moves focus into that content or holds focus there.
When to Use
Use a modal dialog when:
- Users have to make a decision or supply information before they can continue, such as confirming a destructive action.
- The task is short and self-contained, and users don't need the rest of the page to complete it.
- The dialog interrupts with a brief, urgent message that needs a response. Give it the
alertdialogrole.
Do not use a modal dialog when:
- Users need to refer to the page while they work in the dialog. Use a non-modal dialog.
- The message is brief and important, and it shouldn't interrupt what users are doing. Use an alert, which is described in the APG Alert Pattern.
- The content is supplementary and can sit in the page. Use a disclosure.
- Users need a list of actions or commands. Use a menu.
Examples
Code in this section is meant to demonstrate techniques rather than requirements and lacks features needed in a production application.
Both examples build the same confirmation: users activate a "Delete item" button, and a dialog asks them to confirm. The Cancel button takes focus first because it's the least destructive action. The launcher is a native <button>, named for the action the dialog completes. A launcher built from a generic element with role="button" opens the dialog the same way once it meets the Button criteria.
Native Modal Dialog Using the Dialog Element
Calling showModal() does most of the work. The browser makes everything outside the dialog inert, exposes the dialog as modal, moves focus to the element carrying autofocus, closes the dialog when users press Escape, and returns focus to the launcher when the dialog closes. The script only opens and closes it.
Native modal dialog HTML
<button id="launcher" type="button">
Delete item
</button>
<dialog
id="confirm-dialog"
aria-labelledby="confirm-title">
<h2 id="confirm-title">
Delete this item?
</h2>
<p>
You can't undo this.
</p>
<button id="cancel" type="button" autofocus>
Cancel
</button>
<button id="confirm" type="button">
Delete
</button>
</dialog>Native modal dialog JavaScript
const dialog = document.getElementById('confirm-dialog')
document.getElementById('launcher')
.addEventListener('click', () => dialog.showModal())
document.getElementById('cancel')
.addEventListener('click', () => dialog.close())
document.getElementById('confirm')
.addEventListener('click', () => {
// delete the item, then
dialog.close()
})As an alternative to the script, invoker commands open and close the dialog declaratively. The launcher's commandfor attribute names the dialog and command="show-modal" opens it, the same as calling showModal(). A button inside the dialog with command="close" closes it.
Native modal dialog without script HTML
<button
type="button"
commandfor="confirm-dialog"
command="show-modal">
Delete item
</button>
<dialog
id="confirm-dialog"
aria-labelledby="confirm-title">
<h2 id="confirm-title">
Delete this item?
</h2>
<p>
You can't undo this.
</p>
<button
type="button"
commandfor="confirm-dialog"
command="close"
autofocus>
Cancel
</button>
<button
type="button"
commandfor="confirm-dialog"
command="close">
Delete
</button>
</dialog>Custom Modal Dialog Using ARIA
A generic element takes the dialog role and aria-modal="true", and script supplies what the native element does. The dialog sits outside the #page container, so making that container inert leaves the dialog available. The hidden attribute takes the closed dialog out of the accessibility tree and the tab order.
Custom modal dialog HTML
<div id="page">
<button id="launcher" type="button">
Delete item
</button>
</div>
<div
id="confirm-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="confirm-title"
hidden>
<h2 id="confirm-title">
Delete this item?
</h2>
<p>
You can't undo this.
</p>
<button id="cancel" type="button">
Cancel
</button>
<button id="confirm" type="button">
Delete
</button>
</div>CSS centers the dialog and dims the page behind it.
Custom modal dialog CSS
#confirm-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 1.5rem;
background: white;
box-shadow: 0 0 0 100vmax rgb(0 0 0 / 0.5);
}When the dialog opens, JavaScript saves the element that had focus, makes the page inert, and moves focus inside. While the dialog is open, the script wraps Tab and Shift + Tab at the ends and closes the dialog on Escape. Opening a dialog that's already open does nothing. Closing reverses each step and returns focus.
Custom modal dialog JavaScript
const page = document.getElementById('page')
const dialog = document.getElementById('confirm-dialog')
let opener = null
function tabbables() {
return [...dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex="0"]'
)].filter((el) => !el.disabled)
}
function openDialog() {
if (!dialog.hidden) return
opener = document.activeElement
page.inert = true
dialog.hidden = false
tabbables()[0].focus()
}
function closeDialog() {
dialog.hidden = true
page.inert = false
opener?.focus()
}
dialog.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeDialog()
return
}
if (event.key !== 'Tab') return
const items = tabbables()
const first = items[0]
const last = items[items.length - 1]
if (event.shiftKey && document.activeElement === first) {
event.preventDefault()
last.focus()
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault()
first.focus()
}
})
document.getElementById('launcher')
.addEventListener('click', openDialog)
document.getElementById('cancel')
.addEventListener('click', closeDialog)
document.getElementById('confirm')
.addEventListener('click', () => {
// delete the item, then
closeDialog()
})Structure and Semantics
A modal dialog tells users three things: that it's a dialog, what it's for, and that the rest of the page is unavailable while it's open. The role carries the first and the accessible name the second. The third is modality: while the dialog is open, everything outside it is inert, so keyboard, pointer, and assistive technology users can't reach the page behind it until the dialog closes.
The native <dialog> element has the dialog role implicitly. Opened with showModal(), it's exposed as modal and everything outside it is inert. Opened with show() or the open attribute, it's a non-modal dialog, even when it's styled to look like a modal one.
A custom dialog carries role="dialog", or role="alertdialog" for an urgent message that needs a response, along with aria-modal="true". Script makes everything outside it inert. The alertdialog role can go on a native <dialog> element too.
Every element needed to operate the dialog, including its close button, is a descendant of the element carrying the role. The dialog is already the container, so it needs no landmark wrapper.
| Attribute | How to use |
|---|---|
aria-labelledby | Set on the dialog to the id of its visible title. |
aria-label | Names the dialog directly when there's no visible text to point aria-labelledby at. |
aria-modal | In the custom construction, set to true on the dialog element. A native <dialog> opened with showModal() doesn't need it. |
aria-describedby | Optional. Points at a short message stating the dialog's purpose, so it's presented with the name when the dialog opens. Leave it off when the content has lists, tables, or several paragraphs. |
inert | In the custom construction, set on everything outside the dialog while it's open. |
The launcher is a button, so it carries the button role and an accessible name, and conveys a disabled state when it has one, the same way any button does.
What's specific to a launcher is one optional attribute and one it leaves off. aria-haspopup="dialog" conveys that the launcher opens a dialog, so screen reader users know before they activate it. Current versions of JAWS, NVDA, and VoiceOver announce it, but support isn't universal, so test it with the screen readers users rely on.
The launcher carries no aria-expanded: the dialog it opens takes focus and identifies itself, so there's no open state on the button for users to track.
Role
A modal dialog carries the dialog role, or alertdialog for an urgent message that needs a response, on the container that holds all of its content. The native <dialog> element has the role implicitly.
The dialog's launcher carries the button role, because opening a dialog is an action on the current page rather than navigation. A link that opens a dialog tells users to expect a new page, so use a <button> element or role="button".
Has the correct role
Reported as: Element has incorrect role
A modal dialog must convey the dialog or alertdialog role, and its launcher the button role.
When the container has no role, or a generic one, screen reader users who are moved into it hear its content as ordinary page content. Nothing says a dialog opened or that it has to be dealt with first. When the launcher has no role or a link role, users don't know it acts on the page.
Use the <dialog> element, or put role="dialog" on a custom container. Use alertdialog for an urgent message that needs a response. Build the launcher from a <button> element, or give a custom one role="button".
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Applies the role to the dialog container
Reported as: Incorrect root element
Sanity check. Evinced checks that the dialog's role sits on the container that holds its content, and the launcher's on the element users focus and activate. This check runs on every UI pattern, not only on dialogs.
A modal dialog's role should sit on the element that contains all of its content, and the launcher's role on the element that takes focus and handles activation.
When the role is on an inner wrapper while the outer element carries the name, aria-modal, and the dialog's controls, or the other way round, no single element is the dialog. Screen reader users hear an unnamed dialog, and can find its close button reported as outside it. On the launcher, a misplaced role announces a button that isn't the element users operate.
Put the dialog's role, accessible name, and aria-modal on one container, and keep any layout wrapper free of them. Put the launcher's role, name, focus, and activation handling on one element.
Button Name
A modal dialog's launcher is named for the dialog's purpose, usually the action the dialog completes, such as "Delete item" or "Change password", rather than "Open dialog" or "More". The name often matches the dialog's title, and a dialog with no visible title can take its name from the launcher, as Accessibility Label shows.
Has an accessible name
Reported as: Accessible name
A modal dialog's launcher must have an accessible name that says what the dialog is for.
When the launcher is an icon with no text or alt text, screen readers announce only "button", and users can't tell what it opens until they're inside the dialog.
Name the launcher with visible text, an image's alt text, or aria-label, and keep the name short and specific to the dialog's task.
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Accessibility Label
A modal dialog's name is the first thing screen reader users get when it opens, before any of its content, so it says what the dialog is for. Point aria-labelledby at the dialog's visible title, as both builds under Examples do.
The following example shows two alternatives for a dialog with no visible title: naming it from the launcher's text, and naming it with aria-label when there's no text to point at.
<!-- No visible title: name it from the launcher -->
<button id="zip-launcher" type="button">
Find zip code
</button>
<dialog aria-labelledby="zip-launcher">
...
</dialog>
<!-- No visible text to point at -->
<dialog aria-label="Find zip code">
...
</dialog>Names a container that needs one
Reported as: Missing contextual labeling
A modal dialog must have an accessible name that says what it's for.
When the dialog has no name, screen reader users are moved into it and told only that they're in a dialog. On a page that can open several, they can't tell which one appeared, and they have to read its content to find out what it's asking.
Name the dialog from its visible title with aria-labelledby. When it has no title, point aria-labelledby at the launcher's text, or use aria-label when there's no visible text to point at. Name it for its task, such as "Delete this item?", rather than "Dialog".
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
aria-modal
A custom modal dialog declares its modality with aria-modal="true", so assistive technology can keep users within it. A native <dialog> opened with showModal() is exposed as modal without the attribute.
Mark a dialog modal only when it behaves modally for everyone, with the page behind it inert and visually obscured. Some assistive technologies hide the page once the attribute is set, so a dialog that claims modality without delivering it cuts their users off from content everyone else can reach.
Declares itself modal
Reported as: Missing aria-modal attribute
A custom modal dialog should carry aria-modal="true" on the element with the dialog role.
When the attribute is missing, nothing tells assistive technology that the page outside is meant to be unavailable. Tab may stay inside, but screen reader users reading by line or by heading aren't bound by focus traps, and nothing suggests they should stay.
Set aria-modal="true" on the dialog element itself, not on a wrapper. When the closed dialog stays rendered, remove the attribute on close. The attribute declares modality and doesn't create it, so still make the page outside inert.
Severity: Minor
Disabled
A modal dialog never takes a disabled state. When part of its task isn't available, disable the controls inside it rather than the dialog.
The dialog's launcher is disabled when the task its dialog performs isn't available yet, such as a "Delete item" button with nothing selected. Say why, and point the launcher's aria-describedby at the explanation. Don't disable the launcher while its dialog is open: a disabled native button can't take focus, so the dialog has nowhere to return it when it closes.
Disables only when role allows
Reported as: Component does not support disabled state
A modal dialog must not declare a disabled state, which the dialog and alertdialog roles don't support.
When disabled or aria-disabled is set on the dialog, screen reader users can hear it described as unavailable while every control inside it still works, or hear nothing at all of what the design meant.
Remove the attribute from the dialog, and disable each control inside it that's unavailable.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Disables only elements that support it
Reported as: Element type does not support disabled state
An element in a dialog must carry disabled or aria-disabled only when its role supports a disabled state.
When the state sits on an element whose role has none, such as a wrapper, a list item or a heading, assistive technology may drop it or report it on something users can't operate, while the control that really is unavailable is still reported as available.
Remove the attribute from that element, and set the disabled state on the control itself, using an element or role that supports it, such as a native <button>.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Avoids conflicting disabled states
Reported as: Conflicting disabled values
A modal dialog's launcher must declare its disabled state one way only: the disabled attribute on a native <button>, or aria-disabled on a custom launcher.
When a native launcher carries aria-disabled instead of disabled, it still opens the dialog while assistive technology reports it as unavailable. When both attributes are set and disagree, screen reader users are told the wrong thing about whether the dialog can be opened.
Use disabled alone on a native launcher and aria-disabled alone on a custom one, removing it or setting it to false when the launcher is enabled again.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Uses aria-disabled rather than disabled
Reported as: Element does not support the disabled attribute
A custom launcher built from an element that doesn't support the disabled attribute must convey its disabled state with aria-disabled.
When disabled is set on a <div> or <span> launcher, browsers ignore it. The launcher looks unavailable, but assistive technology reports it as enabled, and keyboard and screen reader users can still open a dialog whose task isn't meant to be available.
Set aria-disabled="true" instead, and block activation in the launcher's event handlers while it's set.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Screen Readable
A modal dialog is present for assistive technology exactly when it's present on screen. When it closes, hide it from both with the hidden attribute, display: none, or by removing it from the page. Each of these also takes its content out of the tab order. When the closed dialog has to stay rendered, for example while it fades out, it also needs aria-hidden="true" and inert for that period.
Exposes the dialog when it opens
Reported as: Modal is not available to assistive technology
An open modal dialog must be in the accessibility tree, with the dialog or alertdialog role.
When the dialog keeps aria-hidden="true" in its open state, or sits inside a hidden ancestor, screen reader users hear an unchanged page that has stopped responding, with no sign a dialog opened. When it's present without a role, its content reads as ordinary page content, and nothing says it has to be dealt with first.
Give the container the dialog role, or alertdialog for an urgent message. When it opens, clear aria-hidden from it and its ancestors, and remove any hidden attribute or display: none.
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Reported as: Hidden modal available to assistive technology
A closed modal dialog must be out of the accessibility tree.
When the dialog is closed by moving it off screen or fading it to transparent, its heading, text, and buttons stay in the page for screen reader users, who meet a dialog nobody opened and can't tell it from live content. Keyboard users can tab into it, and focus seems to vanish.
Take the dialog out of the render tree when it closes, with the hidden attribute, display: none, or by removing it. When it has to stay rendered, set aria-hidden="true" and inert on it until it opens again.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Forbidden Interactive Children
A modal dialog's launcher holds only its label, as text or as an icon with alternative text. The dialog is a separate element elsewhere in the page, never inside the launcher, so the dialog's controls are never among the launcher's children.
Avoids nested interactives
Reported as: Forbidden interactive children; Nested interactive elements
A modal dialog's launcher must not contain, or be nested inside, another interactive element.
A button's children are presentational. When the launcher holds a link or another button, such as a dialog rendered inside it, screen reader users can't perceive or operate that control, and pointer users can't tell which one their click reaches.
Keep only the label inside the launcher, and place the dialog and any other controls outside it.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Relationships
A launcher, usually a button, opens a modal dialog and controls it. The two are separate elements. The dialog usually sits elsewhere in the DOM, often as a direct child of <body>, and it's out of the accessibility tree until the launcher opens it.
Focus carries that relationship rather than an attribute. Activating the launcher moves focus into the dialog, which identifies itself by its role and name, and closing the dialog sends focus back to the launcher or on to the next step of the task.
ARIA allows aria-controls on the launcher, and when it references the dialog's id, it marks the relationship. It does nothing for assistive technology users, though: activating the launcher already takes them into the dialog, so they never need the reference. The launcher needs no aria-expanded either, and aria-haspopup="dialog" is optional, as Structure and Semantics describes.
Within the dialog, structure does the work. Every element needed to operate it is a DOM descendant of the element carrying the role, which conveys that they belong to it. If content that belongs in the dialog is rendered somewhere else, move it inside rather than claiming it with aria-owns, which some assistive technologies don't support.
aria-owns
A modal dialog holds its content as DOM descendants, not through aria-owns. When content that belongs in the dialog is rendered somewhere else, move it inside rather than claiming it.
The dialog's launcher doesn't own the dialog either. The dialog is a separate window the launcher opens, and claiming it with aria-owns would make the dialog's content presentational children of a button.
Avoids aria-owns
Reported as: Avoid aria-owns
General check. Evinced checks that neither the dialog nor its launcher uses aria-owns to claim elements elsewhere in the document as its children. This check runs on every UI pattern, not only on dialogs.
A modal dialog and its launcher should not use aria-owns.
When the dialog owns content rendered elsewhere, some assistive technologies don't present it as part of the dialog, and it can stay inert with the page. When the launcher owns the dialog, the dialog's controls become presentational children of a button, and screen reader users can't operate them.
Move content that belongs in the dialog inside it, and keep the dialog outside the launcher. When the launcher needs to reference the dialog, use aria-controls.
Operation
Users operate a modal dialog's launcher as they would any button: they click or tap it, or press the Tab key to reach it and press Enter or Space. The launcher has no state to update. While the dialog is open, the launcher is inert with the rest of the page.
Users interact with a modal dialog only from inside it, and leave it only by closing it. When it opens, focus moves to an element inside it. The Tab key and Shift + Tab move through its interactive elements, wrapping at either end, and never reach the page behind. Escape closes it. When it closes, focus returns to the launcher, or to the element that carries the task on.
Every dialog should also include a visible close or cancel button in its tab order. It's the way out for pointer users and for anyone who doesn't know about Escape.
A native <dialog> opened with showModal() provides all of this except the close button. A custom dialog provides it in script, as the build under Examples shows.
Focus Trap
A modal dialog contains its tab order: Tab and Shift + Tab move only among its interactive elements and wrap from one end to the other. A native <dialog> opened with showModal() does this for you. A custom dialog handles the two keys itself, as the build under Examples does, or uses a library such as focus-trap.
Contains its tab order
Reported as: Focus kept within the modal
While a modal dialog is open, Tab and Shift + Tab must keep focus inside it, wrapping at the ends.
When Tab from the last control moves on to the page behind, focus lands somewhere the interface says is unavailable, often hidden by the overlay. Keyboard users press on looking for it, and can operate controls behind the dialog without realizing it.
Use showModal(), or handle both keys at the dialog: from the last tabbable element, Tab moves to the first, and from the first, Shift + Tab moves to the last. Find the tabbable elements on each press, so content added while the dialog is open is included.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.2 Meaningful Sequence (A) | When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. |
| EN 301 549 | 9.1.3.2 Meaningful Sequence | Same as the WCAG text. |
Moves focus on Tab
Reported as: Could not simulate tab navigation
Sanity check. Evinced checks that pressing Tab inside the open dialog moves focus from one of its elements to another.
The Tab key should move focus between the interactive elements inside a modal dialog.
When the dialog's controls are generic elements with click handlers, or a focus trap cancels the Tab key without moving focus itself, focus stays where it landed. Keyboard users can't reach the other controls, often including the close button, and the dialog seems frozen.
Build the dialog's controls from native interactive elements, or give custom ones tabindex="0". When a handler intercepts Tab, have it move focus to the next or previous element rather than only blocking the default behavior.
Focus Sequence
A modal dialog's launcher is a single tab stop, reachable with the Tab key whenever it's enabled. It stays in the page while its dialog is open, inert behind the dialog but in place, so focus can return to it when the dialog closes. Removing or replacing the launcher when the dialog opens leaves the dialog nowhere to send focus back to.
Reachable by keyboard
Reported as: Keyboard accessible
A modal dialog's launcher must be reachable in the keyboard tab order.
When the launcher is built from markup that isn't focusable, such as a <div> or <span>, pointer users can open the dialog, but keyboard users can't reach the launcher or the task behind it.
Use a native <button> element, which is focusable by default, or give a custom launcher tabindex="0".
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Skips tab order when disabled
Reported as: Disabled element focusable
A disabled launcher should be out of the tab order, as a disabled native <button> is.
When a disabled custom launcher stays in the tab order, keyboard users land on a control that won't open its dialog.
Set tabindex="-1" on a custom launcher while aria-disabled="true" is set, and tabindex="0" when it's enabled again.
Severity: Minor
Element that carries role receives focus
Reported as: Focusable offset
The element that carries the launcher's button role and opens the dialog must be the one that receives focus, not a descendant or a wrapper.
When another element takes focus instead, keyboard users can tab to the launcher but may not be able to open the dialog with Enter or Space.
Use a native <button>, or set tabindex="0" on the custom element that carries the role and handles activation.
Severity: Minor
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Focus Order
A modal dialog moves focus twice, in when it opens and out when it closes, and where it lands decides whether users can carry on without searching for their place.
On close, focus usually returns to the launcher, so the launcher has to stay in the page and focusable, as Focus Sequence covers. When the launcher is gone, or when the dialog's task leads to a next step, focus goes to the next logical element in the workflow instead.
Returns focus to the launcher
Reported as: Focus management on modal close
Closing a modal dialog must return focus to the element that opened it.
When nothing restores focus, it drops to the document body and the tab order restarts at the top of the page. Keyboard users tab back through everything to find their place, and screen reader users have to work out where they were before they can go on.
Save a reference to the element that had focus before moving focus into the dialog, and call focus() on it after the dialog is hidden. Use the same path for every way of closing it, including the close button, Escape, and a submitted form. When the dialog's task leads to a next step, focus that step instead.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.2 Meaningful Sequence (A) | When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. |
| EN 301 549 | 9.1.3.2 Meaningful Sequence | Same as the WCAG text. |
Moves focus into it on open
Reported as: Focus on modal open
Opening a modal dialog must move focus to an element inside it.
When focus stays on the launcher, the next Tab press moves through the page behind, and the focus indicator may be hidden by the overlay. Screen reader users get no sign the dialog opened and go on reading a page that no longer responds.
Move focus inside as part of opening, usually to the first focusable element. When the content has to be read first or would scroll out of view, put tabindex="-1" on the title or first paragraph and focus that. When the dialog confirms something hard to undo, set focus on the least destructive action.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.2 Meaningful Sequence (A) | When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. |
| EN 301 549 | 9.1.3.2 Meaningful Sequence | Same as the WCAG text. |
Leaves focus in a logical place
Reported as: Focus on modal closed
Closing a modal dialog must leave focus on the launcher or, when the launcher is gone or the task continues elsewhere, on the next logical element.
When the dialog's action removes its launcher, the saved reference points at nothing and focus falls to the document body. Users lose their position and the context the dialog was part of. When the dialog completes a step that leads into another, returning to the launcher sends users back to where they started.
Decide the destination for each way of closing. When the launcher is gone, focus the nearest equivalent, such as the row after a deleted one. When the dialog leads into the next step of a task, focus that step even when the launcher is still there, making the element focusable if it isn't.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.2 Meaningful Sequence (A) | When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. |
| EN 301 549 | 9.1.3.2 Meaningful Sequence | Same as the WCAG text. |
Inert Background
Inertness is what makes a dialog modal: while it's open, users can't operate anything outside it, and assistive technology doesn't present it. Dimming the page shows this visually, but a page that is only covered is still there for keyboard and screen reader users. Keep a custom dialog outside the content you make inert, ideally as a direct child of <body>, so it doesn't become inert along with the page.
Makes the rest of the page inert
Reported as: Modal background is not inert
While a modal dialog is open, everything outside it must be unavailable to operate and to read.
When the page behind is only dimmed, screen reader users who move by heading or landmark leave the dialog without noticing and carry on into the page, where controls may still respond. Content from the dialog and the page arrives in one sequence, and getting back means finding a dialog the page gives no sign of.
In the native construction, open the <dialog> element with showModal(), which makes everything outside it inert until it closes. In the custom construction, set inert on everything outside the dialog while it's open, and remove it when the dialog closes.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.2 Meaningful Sequence (A) | When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. |
| EN 301 549 | 9.1.3.2 Meaningful Sequence | Same as the WCAG text. |
Keyboard Activation
Pressing Enter or Space on a modal dialog's launcher opens the dialog, the same as a click. Route every way of activating it through one handler, so the dialog always opens the same way and records the launcher as the place to return focus.
Operable by keyboard
Reported as: Button not activated by keyboard
Keyboard users must be able to open a modal dialog from its launcher.
When a custom launcher responds only to click events from a pointer, keyboard users can reach it but can't open the dialog or complete its task.
Use a native <button> element, which is keyboard operable. For a custom launcher, add keyboard event listeners that open the dialog through the same handler as a click.
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Ignores keyboard when disabled
Reported as: Disabled button keyboard activation
A launcher that reports itself as disabled should not open its dialog from the keyboard.
When a custom launcher carries aria-disabled="true" but Enter or Space still opens the dialog, screen reader users are told the launcher is unavailable and then open its dialog anyway, so they stop trusting the states the interface reports.
Use a native <button>, which blocks activation when disabled is set, or block it in a custom launcher's handlers while aria-disabled="true" is set.
Severity: Serious
Activates with Enter and Space
Reported as: Button requires space key activation
A custom launcher must open the dialog with the Space key, as a <button> element does, not only with Enter.
When only Enter works, users who press Space, as they would on any other button, conclude the launcher is unresponsive.
Use a native <button> element, or add a Space keyup handler to the custom launcher.
Severity: Minor
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Escape Key
Escape is the key users reach for to back out, and in a modal dialog it closes the dialog from wherever focus is inside it, the same way a cancel button would. A native <dialog> opened with showModal() does this for you. When dialogs are stacked, Escape closes only the topmost one.
Closes with the Escape key
Reported as: Modal is not dismissed with Escape key
Pressing Escape must close a modal dialog from anywhere inside it.
When there's no handler, or the handler is attached to one control such as the close button, closing depends on finding that button. With the page behind inert, users who can't reach it have no way back.
Use showModal(), or listen for Escape on the dialog container so the press is caught wherever focus is. Treat it as cancel: discard unsaved input as the cancel button would, or ask first, and return focus as any other close does.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Event Listener Type
When script in a dialog responds to keys, it listens for keydown or keyup. The keypress event is deprecated, and browsers don't fire it for many keys, including the arrow keys, Home, End and Escape.
Avoids the keypress event
Reported as: Deprecated keypress listener
A dialog's key handling should use keydown or keyup, not keypress.
When a handler listens for keypress, keys the browser doesn't send through that event go unanswered, and keyboard users find the dialog ignoring keys it's meant to respond to.
Replace each keypress listener with a keydown listener, and read the key from event.key.
Severity: Minor
FAQ
Which WCAG success criteria do the modal dialog issues relate to?
The dialog's role, the launcher's role and name, the launcher's disabled state, and nothing interactive inside the launcher relate to 4.1.2 Name, Role, Value. The dialog's accessible name and its presence in the accessibility tree only while it's open relate to 1.3.1 Info and Relationships. Moving focus in and back out, keeping it inside, and making the page behind inert relate to 1.3.2 Meaningful Sequence. Reaching and activating the launcher by keyboard, and closing the dialog with Escape, relate to 2.1.1 Keyboard.
Three criteria map to no WCAG success criterion and are best practices: aria-modal on a custom dialog, removing a disabled launcher from the tab order, and ignoring the keyboard when the launcher is disabled.
Why are some modal dialog issues marked as Critical and others as Minor?
Severity reflects how much an issue gets in users' way. Critical issues stop users outright: a dialog or launcher with the wrong role, a launcher with no name or one keyboard users can't reach or activate, or an open dialog that isn't exposed to assistive technology. Serious issues mislead users or let them stray out of the dialog, such as a page behind that isn't inert, focus that isn't moved in, kept in, or returned, or a wrong disabled state on the launcher.
Moderate issues make users work harder, such as a dialog with no name, a closed dialog left in the accessibility tree, or no Escape key. Minor issues break a convention without blocking the task, such as a missing aria-modal or a launcher that ignores the Space key. Three checks carry no severity: the general check for aria-owns and the sanity checks on where the role sits and on Tab moving focus.
Related Patterns
- Button: the element a modal dialog's launcher is built on
- Menu Button: a button that opens a menu and reports whether it's open
- Disclosure: reveals content in the page without moving focus into it
- Menu: a list of actions or commands, rather than a task to complete
Recommended Reading
- Dialog (Modal) Pattern | W3C ARIA Authoring Practices Guide
- Button Pattern | W3C ARIA Authoring Practices Guide
<dialog>: The Dialog element | MDN- ARIA:
dialogrole | MDN - ARIA:
alertdialogrole | MDN - ARIA:
aria-modalattribute | MDN - Use the dialog element (reasonably) | Scott O'Hara
- Where to Put Focus When Opening a Modal Dialog | Adrian Roselli
- Should a Dialog Close When Clicked Outside? | Adrian Roselli