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 dialog with aria-modal="true" — content outside is inert; focus is contained; Escape closes; focus returns to the invoker. The native dialog element opened with showModal() provides most of this.
  • Non-modal dialog (show() or role="dialog" without aria-modal) — users can move between the panel and the page; focus is not trapped; F6 or a shortcut helps move between them.
  • complementary / region landmarks — 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.
Behaviour of the three panel types Matrix comparing modal dialog, non-modal dialog and region on focus on open, Tab containment, Escape, background interactivity and screen reader context. Behaviour of the three panel typesFocus on openTab containedEscapeBackground usableModal dialogInto panelYesCloses, returnsfocusNo (inert)Non-modal dialogInto panelNoCloses, returnsfocusYesRegion / complementaryOnly if activatedNoBack to list(optional)Yes
Pick the row whose behaviour matches what the panel looks like.

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.

Choosing the container Flow: must the user finish in the panel first? Modal dialog. Does it open and close per record while the table stays usable? Non-modal dialog. Is it always present as part of the layout? Region. Choosing the containerMust finish in the panel first?unsaved edits, confirmationsmodal dialogOpens and closes per record?table should stay usablenon-modal dialogAlways present in the layout?content follows selectionregion (complementary)Looks modal (dimmed)?must behave modalinert background

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 &amp; 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 &amp; 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 &amp; 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 —
A drawer that looks non-modal but traps focus Comparison of mismatched implementation versus matched: visible clickable table with trapped focus versus a non-modal dialog with free Tab movement and F6. A drawer that looks non-modal but traps focus✗ Mismatched: looks open, traps focusMouse users click the table freelyKeyboard users cannot Tab to the tableScreen readers told "modal"Escape is the only way out✓ Matched: non-modal dialogSame for everyoneTab and F6 reach the tableNo aria-modal; background readableEscape, Close button, or simply Tab away

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.

Permalink to "Related"

← Back to Master-Detail & Split Views