Overview
Non-modal dialogs differ from modal dialogs in one critical way: the background content remains fully interactive. Users can still tab to, click, and interact with the rest of the page while the dialog is open. The native <dialog> element supports this via .show() (as opposed to .showModal() which creates a modal). Non-modal dialogs are ideal for chat widgets, editing panels, floating toolbars, and any supplementary UI that should not block the user’s workflow.
WCAG Criteria:
- 4.1.2 Name, Role, Value — the dialog must have a role and an accessible name
- 2.1.1 Keyboard — all functionality must be operable via keyboard, including opening, interacting with, and closing the dialog
Key requirements:
- Use the native
<dialog>element with.show()for non-modal dialogs (not.showModal()) - Label the dialog with
aria-labelledbypointing to its heading - Move focus to the dialog when it opens
- Close on Escape key and return focus to the trigger element
- Do not trap focus — the user must be able to Tab out of the dialog into the rest of the page
- Background content must remain fully interactive (no backdrop, no
inert)
Non-Modal Panel
Custom Div Panel vs. Native Non-Modal Dialog
Inaccessible
<!-- Custom div panel — no role, no focus management, no Escape -->
<button onclick="togglePanel()">Chat</button>
<div id="panel" style="display:none">
<div>Chat Support</div>
<p>Hi there! How can we help you today?</p>
<button onclick="closePanel()">Close</button>
</div>Live Preview
Accessible
<!-- Native <dialog> with .show() — non-modal, no backdrop -->
<button id="trigger" onclick="
var dlg = document.getElementById('dlg');
if (!dlg.open) { dlg.show(); dlg.querySelector('h2').focus(); }
else { dlg.close(); }
">
Chat
</button>
<dialog
id="dlg"
aria-labelledby="dlg-title"
onclose="document.getElementById('trigger').focus()"
onkeydown="if (event.key === 'Escape') this.close()"
>
<h2 id="dlg-title" tabindex="-1">Chat Support</h2>
<p>Hi there! How can we help you today?</p>
<button onclick="this.closest('dialog').close()">Close</button>
</dialog>Live Preview
What’s wrong with the custom div panel?
- No
dialogrole — screen readers announce it as generic content, not as a dialog - No
aria-labelledby— screen readers cannot identify the purpose of the panel - No focus management — when the panel opens, focus stays on the trigger button and keyboard users must guess where the panel appeared
- No Escape key handling — keyboard users cannot dismiss the panel without tabbing to the Close button
- No focus restoration — when the panel closes, focus is not returned to the trigger
What the native <dialog> with .show() gives you:
- Built-in
dialogrole in the accessibility tree .show()opens it non-modally — no backdrop, noinerton background, no focus traparia-labelledbylinks the dialog to its heading for a clear accessible name- Focus moves to the heading on open so screen readers immediately announce the dialog context
tabindex="-1"on the heading allows it to receive programmatic focus without entering the tab order- Escape key closes the dialog and the
oncloseevent restores focus to the trigger - Users can Tab freely out of the dialog into the rest of the page
What the screen reader announces:
| Version | Announcement |
|---|---|
| Custom div | ”Chat Support” (just text — no dialog context) |
Native <dialog> with .show() | ”Chat Support, heading level 2, dialog” (dialog role and heading announced) |
.show() vs .showModal() — key differences:
| Behavior | .show() (non-modal) | .showModal() (modal) |
|---|---|---|
| Background interactive | Yes | No (background is inert) |
| Focus trap | No — Tab can leave the dialog | Yes — focus is trapped inside |
| Backdrop | None | ::backdrop pseudo-element |
| Escape closes | Yes (with manual handler) | Yes (automatic) |
| Top layer | No | Yes |