Sandbox
The modal dialog
Without focus management, people who use the keyboard end up behind the modal dialog, in a page they can no longer see. The dialog element handles that focus on its own, and since December 2025 a button can open it with no script in every recent browser.
- Built with
- dialog and command
- Browsers
- Chrome 135+, Firefox 144+, Safari 26.2+
- JavaScript
- None, 5 lines as a fallback
Demo
Anatomy
button command="show-modal"The button that opens the dialog, tied to it bycommandfor.dialogThe dialog, placed above the whole page.h2 autofocusThe heading, which names the dialog and gets focus when it opens.- Content The sentence that says what deleting leads to.
form method="dialog"The Cancel and Delete buttons, which close the dialog with no script. Thevalueof the chosen button goes intoreturnValue.::backdropThe backdrop laid over the page.
HTML code
<!-- Trigger -->
<button
type="button"
commandfor="delete-dialog"
command="show-modal">
Delete the draft
</button>
<!-- Modal dialog -->
<dialog id="delete-dialog" aria-labelledby="delete-dialog-title">
<!-- Heading: names the dialog, and autofocus puts focus on it when it opens -->
<h2
id="delete-dialog-title"
tabindex="-1"
autofocus>
Delete this draft?
</h2>
<p>The draft will be deleted for good.</p>
<!-- Both buttons close the dialog, then focus goes back to the trigger.
dialog.returnValue gets the value of the chosen button -->
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>dialog::backdrop {
background: rgb(0 86 59 / 0.45);
}
/* The page no longer scrolls behind the open dialog.
scrollbar-gutter keeps room for the scrollbar: the page does not shift */
html:has(dialog:modal) {
overflow: hidden;
scrollbar-gutter: stable;
}Usage
Use it for
- A short action that needs an answer before going on, such as confirming a deletion.
- Content that only makes sense at the moment you click the button.
Avoid it for
- Letting the page scroll behind the dialog. The mouse wheel or a finger moves inert content, and you lose your place when it closes:
html:has(dialog:modal)blocks scrolling, with no script. - A dialog that opens on its own when the page loads.
- A Close button shaped like a cross with no accessible name.
<div role="dialog">instead ofdialog: you then have to handle focus and Escape by hand.- Long content that people would want to read while keeping the page in view.
Accessibility
ARIA roles and attributes
aria-labelledby is the only ARIA attribute in the demo. Opened in modal mode, the dialog does without aria-modal.
| Attribute | Element | Description |
|---|---|---|
commandfor="delete-dialog" | button | Points to the dialog to open through its id. The button keeps a name that says what it triggers: "Delete the draft". |
command="show-modal" | button | Opens the dialog in modal mode, with no script. When it closes, focus goes back to this button. |
dialog role (implicit) | dialog | The screen reader gets a dialog, modal since show-modal opened it. |
aria-labelledby="delete-dialog-title" | dialog | Gives the dialog the name of its heading: when it opens, the screen reader announces "Delete this draft?, dialog". |
id="delete-dialog-title" | h2 | The target of aria-labelledby. If the heading text changes, the dialog name follows. |
tabindex="-1" | h2 | Lets the heading get focus, without putting it in the tab order. |
autofocus | h2 | Puts focus on the heading when the dialog opens: the screen reader goes on with "Delete this draft?, heading level 2". |
showModal() | Rest of the page | Modal mode, started by command="show-modal" or by the fallback, makes the page inert while the dialog is open: not read, not clickable, not reachable with the keyboard. |
Keyboard navigation
| Key | Action |
|---|---|
| Enter or Space on the button | Opens the dialog. Focus lands on the heading |
| Tab and Shift+Tab | Move through the Cancel and Delete buttons. Focus can go through the browser address bar, never through the page behind |
| Escape | Closes the dialog. Focus goes back to the button that opened it |
Tab order
- The "Delete the draft" button, on the page.
- The dialog heading, when it opens. It gets focus only once, Tab does not come back to it.
- The "Cancel" button.
- The "Delete" button. Tab then goes back to "Cancel", sometimes after the browser address bar.
With a screen reader
Example in Chrome with VoiceOver in English. The wording changes from one screen reader to another.
| Action | Announcement |
|---|---|
| Enter on the button | "Delete this draft?, dialog", then "Delete this draft?, heading level 2" |
| Tab | "Cancel, button" |
| Escape | "Delete the draft, button" |
Related RGAA criteria
The RGAA is the French accessibility standard. Its 106 criteria test WCAG 2.1 level AA.
- Criterion 7.1: Is each script, where needed, compatible with assistive technologies?
- The JavaScript fallback only calls
showModal(): the role, the name and the modal state stay those ofdialog. - Criterion 10.7: On each web page, for each element that receives focus, is the focus visible?
- Focus stays visible on the heading and on both buttons.
- Criterion 12.8: On each web page, is the tab order consistent?
- Tab only goes through the Cancel and Delete buttons, never the page behind. When the dialog closes, focus resumes on the button that opened it.
- Criterion 12.9: On each web page, navigation must not contain any keyboard trap. Is this rule met?
- No keyboard trap: Escape and the Cancel button always let you out.
Other references
WCAG 2.2
EN 301 549
- Accessibility requirements for ICT products and services (V4.1.1, PDF)Clauses 9.2.1.1, 9.2.1.2, 9.2.4.3, 9.2.4.7, 9.4.1.2
APG pattern
Beyond the APG
| New feature | What it adds | Browsers | In the demo |
|---|---|---|---|
command and commandfor | Open and close the dialog from a button, with no script. | Chrome 135+, Firefox 144+, Safari 26.2+ | Yes |
closedby | Closes the dialog with a click on the backdrop, with no script. | Chrome 134+, Firefox 141+ | Variation |
Variations
Close on backdrop click
The closedby="any" attribute also closes the dialog on a click outside it. It does not exist in every browser yet: keep the Cancel button.
<dialog
id="delete-dialog"
closedby="any"
aria-labelledby="delete-dialog-title">Fallback for older browsers
Before December 2025, not every browser supported command (Safari added it last, in version 26.2). A short script takes over: on a click on the button, dialog.showModal() opens the dialog.
The demo above already includes this fallback: try it in a browser from before December 2025.
<script>
// Fallback: browsers from before December 2025, which do not support command
if (!('commandForElement' in HTMLButtonElement.prototype)) {
const trigger = document.querySelector('[commandfor="delete-dialog"]');
const dialog = document.getElementById('delete-dialog');
trigger.addEventListener('click', () => dialog.showModal());
}
</script>