Record Inspector Panels — Dialog or Region?
Permalink to "Record Inspector Panels — Dialog or Region?"Data products love the slide-in inspector: click a row, and a panel slides in from the right with the record’s full details, comments and actions. Visually it is the same component whether it blocks the page or not. Semantically, it has to be one of three things — a modal dialog, a non-modal dialog, or a region — and each implies different keyboard behaviour. The most common bug is a panel that looks non-modal (the table is still visible and clickable) but traps focus like a modal, or a panel that looks modal (dimmed background) but lets Tab wander behind it.
This guide helps you choose and implement. It belongs to master-detail & split views.
Spec reference
Permalink to "Spec reference"- WAI-ARIA
dialogwitharia-modal="true"— content outside is inert; focus is contained; Escape closes; focus returns to the invoker. The nativedialogelement opened withshowModal()provides most of this. - Non-modal
dialog(show()orrole="dialog"withoutaria-modal) — users can move between the panel and the page; focus is not trapped; F6 or a shortcut helps move between them. complementary/regionlandmarks — a persistent panel that is part of the layout; no open/close focus rules beyond what the user triggers.- SC 2.4.3 Focus Order (A), SC 2.1.2 No Keyboard Trap (A) and SC 1.3.1 Info and Relationships (A) apply in every case.
When to use — and when not to
Permalink to "When to use — and when not to"Use a modal dialog when the panel holds a task that must finish before the user does anything else — editing a record with unsaved changes, confirming a destructive bulk action. Dim the background and make it truly inert.
Use a non-modal dialog when the panel is transient (it opens and closes per record) but users benefit from moving back to the table while it is open — comparing values, picking the next row.
Use a region when the panel is a permanent part of the layout, as in a three-column email client: always present, its content changing with the selection. That is the master-detail pattern proper.
The misapplication: a non-modal-looking panel implemented with a focus trap library because “it is a drawer”. Users can see and click the table but cannot Tab to it.
Annotated code example
Permalink to "Annotated code example"<!-- Option A: modal inspector -->
<dialog id="inspector" aria-labelledby="insp-h">
<h2 id="insp-h">Edit Lund & Co — INV-1042</h2> <!-- names the record -->
<form method="dialog">…<button value="cancel">Cancel</button><button value="save">Save</button></form>
</dialog>
<!-- Option B: non-modal inspector -->
<aside id="peek" role="dialog" aria-labelledby="peek-h" hidden> <!-- no aria-modal -->
<h2 id="peek-h" tabindex="-1">Lund & Co — INV-1042</h2>
<button type="button" class="close" aria-label="Close details for INV-1042">×</button>
</aside>
<!-- Option C: persistent region -->
<aside aria-labelledby="detail-h"><h2 id="detail-h" tabindex="-1">Lund & Co — INV-1042</h2>…</aside>
// A: modal — showModal() makes the rest of the page inert and handles Escape
openBtn.addEventListener('click', () => { inspector.showModal(); });
inspector.addEventListener('close', () => rowButton(lastId).focus()); // return focus (SC 2.4.3)
// B: non-modal — move focus in, no trap, Escape closes, F6 cycles panes
function openPeek(id) { lastId = id; peek.hidden = false; peekHeading.focus(); }
peek.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { peek.hidden = true; rowButton(lastId).focus(); }
});
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Panel type | Event | Expected announcement | AT deviations |
|---|---|---|---|
| Modal | Open | “Edit Lund & Co — INV-1042, dialog” + first field | VoiceOver may read the dialog’s entire text |
| Modal | Tab at last control | Wraps to first control in dialog | — |
| Non-modal | Open | “Lund & Co — INV-1042, dialog, heading level 2” | Some readers say “dialog” only on entry |
| Non-modal | Shift+Tab from first control | Moves to the page behind | Expected — not a bug |
| Region | Arrow in list | Pane updates silently | — |
| Any | Escape | Panel closes (dialogs); focus to row | — |
Integration context
Permalink to "Integration context"Modal behaviour is detailed in native dialog versus custom focus traps and using inert to isolate background content. Returning focus to a row that may have changed is covered in restoring focus after closing complex modals. Moving between panes with F6 is in keyboard navigation between list and detail panes.
Gotchas
Permalink to "Gotchas"Switching rows while a non-modal panel is open. If the user selects another row, update the panel and its heading, but do not move focus — they are in the table.
Panels on narrow screens. A non-modal side panel that becomes full-screen on mobile is effectively modal there. Switch to modal behaviour at that breakpoint, or keep it non-modal but make sure the table is reachable.
Unsaved changes. Closing a panel with unsaved edits needs a confirmation that is itself a modal dialog; return focus to the panel if the user cancels.
Framework notes
Permalink to "Framework notes"Radix UI Dialog is modal by default; pass modal={false} for a non-modal sheet, which disables its focus trap and outside-pointer blocking. MUI Drawer with variant="temporary" is modal; variant="persistent" is not — make sure a persistent drawer is not given role="dialog" with aria-modal. Angular CDK Dialog supports hasBackdrop: false and disableClose, but the focus trap remains unless you configure autoFocus and avoid cdkTrapFocus.
// Radix: a non-modal record peek
<Dialog.Root open={open} onOpenChange={setOpen} modal={false}>
<Dialog.Content aria-describedby={undefined} onOpenAutoFocus={(e) => { e.preventDefault(); headingRef.current?.focus(); }}>
<Dialog.Title ref={headingRef} tabIndex={-1}>{record.title}</Dialog.Title>
</Dialog.Content>
</Dialog.Root>
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Offer three named components — RecordDialog, RecordPeek and RecordPane — instead of one Drawer with a dozen props. Names that encode behaviour make the semantic choice explicit at the point of use, and each can own its focus and Escape rules.
FAQ
Permalink to "FAQ"Is a slide-in drawer a dialog?
If it opens and closes on demand, yes — modal or non-modal. If it is always present, it is a region.
Should non-modal panels have aria-modal="false"?
Omit the attribute; false is the default. What matters is not trapping focus and not making the background inert.
Can the inspector be a separate route?
Yes, and that is often best on small screens. Then treat it as page navigation: focus the heading on arrival and restore the list row on Back.
How do screen reader users know a non-modal panel opened?
Moving focus to its heading announces it. If the panel opens without a focus move (because the user only selected a row), it is a region update — see the guidance on announcing detail updates.
Can one panel switch between modal and non-modal?
It can, for example becoming modal once the user starts editing. If you do this, announce the change (“Editing. Other parts of the page are unavailable until you save or cancel.”) and make the switch visible with a backdrop, so the behaviour never surprises anyone.
Related
Permalink to "Related"- Focus handoff — activation versus selection
- Native dialog versus custom traps — modal mechanics
- Using inert — making the background truly unavailable