Overview
Modal dialogs are one of the trickiest components to get right. The native <dialog> element with .showModal() is now the recommended approach — it provides a built-in focus trap, automatic inert on background content, Escape key handling, and a ::backdrop pseudo-element. The old custom ARIA approach required hundreds of lines of JavaScript to achieve the same result.
WCAG Criteria:
- 2.1.2 No Keyboard Trap — focus must be contained within the modal while it is open
- 2.4.3 Focus Order — focus must move to the dialog when it opens and return on close
- 4.1.2 Name, Role, Value — the dialog must have a role and an accessible name
Key requirements:
- Use the native
<dialog>element with.showModal()(preferred over custom ARIA) - Label the dialog with
aria-labelledbypointing to its heading - Move focus into the dialog on open (native
<dialog>does this automatically) - Return focus to the trigger element on close
- Close on Escape key (native
<dialog>does this automatically)
Modal Dialog
Custom ARIA Modal vs. Native Dialog
Inaccessible
<!-- Custom div modal — no focus trap, no Escape, no inert background -->
<button onclick="document.getElementById('overlay').style.display='flex'">
Delete Account
</button>
<div id="overlay" style="display:none; position:fixed; inset:0;">
<div>
<h2>Are you sure?</h2>
<p>This action cannot be undone.</p>
<button onclick="close()">Cancel</button>
<button onclick="close()">Delete</button>
</div>
</div>Live Preview
Accessible
<!-- Native <dialog> — built-in focus trap, Escape, ::backdrop -->
<button id="trigger" onclick="document.getElementById('dlg').showModal()">
Delete Account
</button>
<dialog
id="dlg"
aria-labelledby="dlg-title"
aria-describedby="dlg-desc"
onclose="document.getElementById('trigger').focus()"
>
<h2 id="dlg-title">Are you sure?</h2>
<p id="dlg-desc">This action cannot be undone.</p>
<button onclick="this.closest('dialog').close()">Cancel</button>
<button onclick="this.closest('dialog').close()">Delete</button>
</dialog>Live Preview
What’s wrong with the custom div modal?
- No
dialogrole — screen readers don’t announce it as a dialog - No focus trap — Tab key moves to elements behind the overlay
- No Escape key handling — keyboard users can’t dismiss it
- Background content remains interactive — users can click/tab behind the modal
- Focus is not managed on open or close
What the native <dialog> gives you for free:
- Automatic focus trap (Tab/Shift+Tab wraps within the dialog)
- Escape key closes the dialog (fires the
closeevent) - Background content is automatically
inert(unfocusable, unclickable) ::backdropCSS pseudo-element for the overlay- Built-in
dialogrole in the accessibility tree
What the screen reader announces:
| Version | Announcement |
|---|---|
| Custom div | ”Are you sure?” (just text — no dialog context) |
Native <dialog> | ”Are you sure?, dialog” (dialog role announced, content described) |
You still need to add:
aria-labelledbypointing to the dialog headingaria-describedbyfor the warning/description text- Focus restoration on close (via the
oncloseevent) - Focus the least destructive action for confirmation dialogs