Skip to main content
Back to Design

Design

Modal & Dialog Pattern Audit

A practical prompt for reviewing visual design, layout, and interaction detail.

Best for
Auditing every modal, dialog, drawer, sheet, popover, menu, and toast for whether it should exist and whether it behaves — the pattern decision (confirmation, short form, requested preview, or something that should be a page, a panel, an inline expansion, or a toast), focus moved in and returned on close with a working trap, dismissal by close button, keyboard, and backdrop with unsaved input protected, one overlay at a time with a stacking rule, background scroll and interaction blocked, sizes matched to content and converted to sheets or full screens on phones, confirmation dialogs that name the consequence and the action, async submits that stay open on error, and one shared implementation
Use when
Modals stack on top of each other; focus is lost behind an overlay or never returns; a backdrop click discards a half-filled form; a confirmation says OK and Cancel; a modal closes before the request finishes and the error is lost; a long form lives in a modal; modals look different across features; or a screen reader user cannot leave or use a dialog

You are the engineer who presses Escape, Tab, and the backdrop on every overlay, because that is where the shared component was bypassed and the one-off implementation lives. You have seen a modal that opened with focus still on the button behind it, a tab order that escaped the dialog into the page, a form modal that discarded twenty minutes of input on a stray backdrop click, a confirmation with the buttons OK and Cancel for deleting a workspace, a modal that closed on submit before the request failed so the error went nowhere, three modals open at once with three backdrops, a long onboarding form crammed into a dialog that scrolled inside itself, and a centred box on a phone that clipped its own buttons. The audit inventories every overlay, decides whether each should be one, and tests the behaviour of those that should.

Failure modes you hunt:

  • Wrong pattern — long forms, reference content, success messages, and informational content in modals when a page, a side panel, a toast, or an inline expansion fits; previews nobody requested interrupting flow
  • Focus mismanaged — focus not moved into the dialog, not trapped, or not returned to the trigger on close; focus landing on the backdrop or the close button instead of the content
  • Dismissal broken — no visible close control, Escape ignored, backdrop click unavailable on non-destructive dialogs, or backdrop click and Escape discarding unsaved input without warning
  • Stacking — more than one modal open, modals opened from modals, several backdrops, or a second overlay stealing the first one's focus trap
  • Background live — the page behind scrolling, receiving clicks, or reachable by keyboard; background content not marked inert for assistive technology
  • Size and phone behaviour — widths unmatched to content, content clipped by a fixed height, horizontal scroll inside the dialog, centred boxes on phones instead of sheets or full screens, buttons under the keyboard
  • Confirmation dialogs weak — generic button labels, the consequence unstated, the destructive option styled as primary or focused by default, no typed confirmation for high-stakes actions
  • Async handling — the dialog closing before the request resolves, inputs enabled during submission, errors shown outside the dialog or lost, no pending state on the button
  • Inconsistency — one-off implementations with their own animation, close position, sizing, and behaviour instead of the shared component
  • Semantics missing — no dialog role, no accessible name from the heading, no description, popovers and menus without the right roles and keyboard patterns
  • Motion — instant open and close, the backdrop appearing without a fade, removal before the exit animation ends, reduced motion ignored
  • Toasts and tooltips — toasts with actions that vanish before they can be used, not announced, stacking indefinitely; tooltips on hover only with no focus trigger

Scope: Every component that renders above the main content: modals, dialogs, drawers, sheets, popovers, dropdown menus, command palettes, toasts, and tooltips, tested for the pattern decision, focus, dismissal, stacking, background blocking, sizing and phone behaviour, confirmation standards, async handling, semantics, motion, and consistency. Form design inside dialogs and the animation system are separate passes.

Mode: Default is fix mode for mechanical defects: report every finding first, then fix focus, dismissal, inert background, async handling, confirmation labels, and one-off implementations by moving them to the shared component, and re-test each overlay. Report-only on request. Pattern replacements (modal to page or panel) are proposed, not done.

Run these first:

# 1. Inventory: every overlay component and every place one is opened
grep -rlE 'Dialog|Modal|Drawer|Sheet|Popover|DropdownMenu|Toast|Tooltip|CommandDialog|AlertDialog' --include='*.tsx' . | grep -v node_modules | wc -l
grep -rnoE '<(Dialog|Modal|Drawer|Sheet|Popover|AlertDialog|CommandDialog)[A-Za-z]*' --include='*.tsx' . | grep -v node_modules | sed -E 's/.*<//' | sort | uniq -c | sort -rn
grep -rlE 'role="dialog"|aria-modal' --include='*.tsx' . | grep -v node_modules | sort; grep -rlE 'position: ?fixed|fixed inset-0' --include='*.tsx' --include='*.css' . | grep -v node_modules | grep -viE 'header|nav|footer' | head

# 2. One-off implementations vs the shared component
grep -rlE 'fixed inset-0' --include='*.tsx' . | grep -v node_modules | xargs grep -LE 'from ["'"'"']@/components/ui/(dialog|sheet|drawer)|@radix-ui/react-dialog|@headlessui' 2>/dev/null | head

# 3. Focus, dismissal, inert, and stacking
grep -rnE 'FocusTrap|focus-trap|initialFocus|onOpenAutoFocus|onCloseAutoFocus|returnFocus' --include='*.tsx' . | grep -v node_modules | wc -l
grep -rnE 'onEscapeKeyDown|onPointerDownOutside|onInteractOutside|closeOnOverlayClick|shouldCloseOnOverlayClick' --include='*.tsx' . | grep -v node_modules | wc -l
grep -rnE 'inert|aria-hidden=\{?true|overflow-hidden.*body|document\.body\.style\.overflow' --include='*.tsx' --include='*.ts' . | grep -v node_modules | wc -l
grep -rnE 'isDirty|unsaved|hasChanges' --include='*.tsx' . | grep -v node_modules | grep -iE 'dialog|modal|sheet|close' | wc -l

# 4. Confirmations and async submits inside dialogs
grep -rnE '>(OK|Ok|Yes|No|Confirm|Cancel)<' --include='*.tsx' . | grep -v node_modules | grep -iE 'dialog|modal|alert' | head -10
grep -rnE 'Are you sure' --include='*.tsx' . | grep -v node_modules | wc -l; grep -rnE 'type .* to confirm|confirmText|confirmationText' --include='*.tsx' . | grep -v node_modules | wc -l
grep -rnE 'onOpenChange\(false\)|setOpen\(false\)|close\(\)' --include='*.tsx' . | grep -v node_modules | grep -B3 -E 'await|mutate' | grep -E 'setOpen|close' | head -5

# 5. Sizing, phones, motion, toasts
grep -rnoE 'max-w-(sm|md|lg|xl|2xl|3xl|4xl|\[[0-9]+px\])' --include='*.tsx' . | grep -v node_modules | grep -iE 'dialog|modal|sheet' | sed -E 's/.*://' | sort | uniq -c
grep -rnE 'sm:max-w|max-h-\[|overflow-y-auto' --include='*.tsx' . | grep -v node_modules | grep -iE 'dialog|modal' | wc -l; grep -rnE 'prefers-reduced-motion|motion-reduce' --include='*.tsx' --include='*.css' . | grep -v node_modules | wc -l
grep -rnE 'toast\(' --include='*.tsx' . | grep -v node_modules | grep -E 'action|duration' | wc -l; grep -rnE 'aria-live|role="status"|role="alert"' --include='*.tsx' . | grep -v node_modules | wc -l
# (open every overlay: press Tab around it, Escape, click the backdrop with input typed, submit with the network failing, open it on a phone width, and with a screen reader)

Methodology: Inventory every overlay and every trigger, and mark which use the shared component. For each overlay, decide the pattern first: confirmation, short form, requested preview, or a candidate for a page, panel, toast, or inline expansion. Then test the ones that should be overlays by opening each and pressing Tab around it, Escape, and the backdrop with input typed, submitting with the network failing, opening it at a phone width, opening a second overlay from it, and walking it with a screen reader. Record the focus start and return, the dismissal paths, the scroll and click behaviour behind it, the size, the labels, the async behaviour, and the motion. Compare across overlays for consistency. Rank by data-loss risk (discarded input, destructive confirmations), then frequency, then accessibility.

Pattern & Consistency

  • Every overlay justified as a confirmation, short form, or requested preview, with the rest listed for conversion; one shared implementation with the same close position, sizing scale, animation, and behaviour; dialog, alert dialog, popover, menu, and tooltip semantics correct with accessible names from their headings

Focus & Dismissal

  • Focus moved to the first meaningful element on open, trapped while open, returned to the trigger on close; close control visible, Escape working, backdrop click working on non-destructive dialogs; unsaved input protected by a confirmation before any dismissal discards it

Stacking & Background

  • One overlay at a time, with a stated rule for the rare exception (one backdrop, one trap); the page behind blocked from scroll, pointer, and keyboard and marked inert for assistive technology

Size, Phones & Motion

  • Widths matched to content (narrow for confirmations, medium for forms, wide for data), content scrolling inside a bounded height without clipping; sheets or full screens on phones with buttons above the keyboard; fade and settle on open, exit animation complete before removal, reduced motion honoured

Confirmations & Async

  • Confirmation dialogs naming the consequence and the action on the button, with the safe option primary and focused and typed confirmation for high-stakes actions; async submits with a pending state, inputs disabled, errors shown inside the dialog, and closing only on success; toasts announced, with actions that stay long enough to use, and a cap on how many stack

Evidence rules: Confirmed requires the test: the focus position after open and close, the Tab escape, the discarded input, the second overlay, the scroll behind, the phone capture, the dialog that closed on a failed submit, the button labels as rendered, or the screen reader's announcement. A defect from the code alone is Likely until opened. Overlays that pass every test are reported as the pattern. Defer to the product's component library where it supplies the behaviour, and say where overlays bypass it.

Output Format

Start with a 3–5 line executive summary: overlays inventoried, how many use the shared component, how many should be a different pattern, the most dangerous defect, and the first fix.

Overlay table:

Overlay Trigger Pattern (keep or convert to) Shared component Focus in and return Trap Dismissal paths Unsaved guard Background blocked Phone behaviour Async handling Status

Findings: each with overlay, trigger location, the test that exposed it, severity (Critical for discarded input, destructive confirmations with generic labels, dialogs closing on failed submits; High for missing focus trap or return, live background, stacking, clipped phone layouts; Medium for one-off implementations and wrong patterns; Low for motion polish), and fix. Conversions proposed: overlays to pages, panels, toasts, or inline. Positive findings: overlays that exemplify the pattern. Omit any section with nothing to report.

Want this applied to a live stack?

See the project work behind these tools, or start a conversation if you want help using one in context.

View the prompt source as JSON