opsinjs
ComponentsSurfaces

Dialog

A window that opens over the page and waits for an answer. It takes your keyboard and attention until you deal with it, and gives them back where it found them.

Preview

Theme
Density
Text
Status
dialog · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/dialog

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Dialog } from "@/components/ui/dialog"
<Dialog
  open={open}
  onOpenChange={setOpen}
  title="Delete this reading?"
  description="It will be removed from your history and from any trends it appears in."
  actions={
    <>
      <Button variant="destructive" onClick={remove}>Delete reading</Button>
      <Button variant="primary" onClick={() => setOpen(false)}>Keep it</Button>
    </>
  }
/>

When to use it

Use it when

  • A decision the reader must make before anything else can proceed.
  • Confirming an action that cannot be undone or that the reader may not have anticipated.
  • A short blocking task, such as re-authenticating before revealing sensitive values.

Do not use it when

  • The reader could carry on without answering, or you are announcing, not asking.Use alert-banner instead.
  • A task the reader chose to start and can abandon.Use sheet instead.
  • Asking permission to collect or share health data.Use consent-sheet instead.
  • Confirming something that has already happened.Use toast instead.
  • Supporting detail attached to a control.Use popover instead.

Anatomy

  1. DialogThe portal root, holding the scrim and the panel.Controlled by open
  2. Dialog.ScrimThe dimming layer, a Surface at the scrim rung.
  3. Dialog.ViewportFixed box; floats low on a phone, centred above sm.
  4. Dialog.ContainerThe popup. Carries the role, the name and the description.Controlled by className
  5. Dialog.HeaderPinned. Holds the title and close control.
  6. Dialog.TitleAn h2, always visible, and the accessible name.Controlled by title
  7. Dialog.CloseGlyph and visible word from closeLabel. Absent when severity is alert.Controlled by severity
  8. Dialog.BodyThe scroll region, so the actions stay reachable at 200% text.
  9. Dialog.DescriptionWhat each answer does, and on an alert dialog why one is needed.Controlled by description
  10. Dialog.ContentOptional. Nothing translucent goes in here.Controlled by children
  11. Dialog.ActionsPinned, least destructive last, at most two. Stacked below sm.Controlled by actions

Examples

Confirming something that cannot be undone

Destructive first, safest last, where initialFocus="safest" lands, so a stray Return keeps the reading.

Theme
Density
Text
Status
dialog-confirming-a-deletion · base base · style base-lyraOpen under the product theme

An answer is needed

severity="alert" sets role="alertdialog", removes the close control, and refuses both the scrim and Escape.

Theme
Density
Text
Status
dialog-an-answer-is-needed · base base · style base-lyraOpen under the product theme

Content guidelines

The title asks the question. The description says what each answer does, and on an alert dialog why one is required. Labels are verbs naming the outcome: never OK and Cancel, never a judgement about the reader.

Do

"Delete this reading? It will be removed from your history. [Delete reading] [Keep it]" Question, consequence, two verbs.

Don’t

"Are you sure? [OK] [Cancel]" Sure about what, and which button does which?

Accessibility

Audited against WCAG 2.2 AA: a source pass and a rendered pass. The rendered pass found no axe violations, no undersized target, no focus-visible miss, and held at 320px. The source pass found no critical, serious or moderate defect. The audit is author-run, not independent, and clinical review is pending.

Confirmed:

  • severity="alert" swaps in Base UI's alert-dialog root, tying the role to the refusal to dismiss.
  • Named by its title, described by its description; a blank description renders nothing.
  • Focus moves in on open, is trapped, and returns on close to whatever had it.
  • The page behind is aria-hidden, not inert, so focus from behind can still reach it.

One residual: Dialog.Close uses a raw focus-visible utility, not the border-focus token, but renders a valid 2px offset ring, so 2.4.7 and 1.4.11 pass; it matches the reviewed sheet close control by design.

KeyActionNotes
TabMoves focus to the next control inside the dialogTrapped; initialFocus="safest" wraps the first Tab to the top.
Shift+TabMoves focus back to the previous controlReverse order matches visual order.
EnterActivates the focused actionWith initialFocus="safest", the answer that changes nothing.
SpaceActivates the focused actionConsumed by the button. The page behind is scroll-locked.
EscapeCloses an ordinary dialog and returns focus to whatever opened itThe scrim dismisses the same way; both refuse when severity is alert.
EscapeDoes not close an alert dialog; moves focus to the dialog itselfRe-reads the named, described container. The reason is not spoken.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
body text on the canvas materiallight104.717.96:1Pass
body text on the canvas materialdark-100.518.00:1Pass
body text on the inset materiallight98.116.32:1Pass
body text on the inset materialdark-100.117.18:1Pass
body text on the card materiallight104.717.96:1Pass
body text on the card materialdark-99.616.32:1Pass
body text on the raised materiallight104.717.96:1Pass
body text on the raised materialdark-97.513.77:1Pass
body text on the sheet material over the darkest backdroplight99.916.76:1Pass
body text on the sheet material over the darkest backdropdark-99.716.47:1Pass
body text on the sheet material over the lightest backdroplight104.717.96:1Pass
body text on the sheet material over the lightest backdropdark-80.86.07:1Pass
body text on the overlay material over the darkest backdroplight85.913.48:1Pass
body text on the overlay material over the darkest backdropdark-99.916.77:1Pass
body text on the overlay material over the lightest backdroplight104.717.96:1Pass
body text on the overlay material over the lightest backdropdark-54.22.69:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from DialogProps in registry/bases/base/dialog.tsx.

actions and closeLabel are not in the specification; closeLabel defaults to Close, ignored on an alert dialog. onOpenChange does not fire when Escape is refused. sheetBelow is not a prop: the dialog never becomes a Sheet.

  • Sheet: a place the reader chose to go and may leave; Dialog never becomes one.
  • AlertBanner: announces without blocking a reader who can carry on.
  • ConsentSheet: a request with a consent contract and a recorded decision.
  • Popover: anchored and non-modal, dismissed by leaving rather than answering.

On this page