Skip to content

Dialog · Scrim

A card that interrupts — one question, one decision, on a scrim over the page.

Beta · Navigation and overlay · packages/css/src/components/dialog.css · React on Base UI: @pepperui/dialog

.pui-scrim (fixed; --static here) · .pui-dialog (--medium --large; __header __texts __title __description __close __content __footer).

Dialog · Scrim — every size and every stateOpen in the workshop

Sizes across, states down.

  • Three widths: small 400 (confirms), medium 480 (forms and choices), large 680 (editor modals)
  • Title, Description, Has description, Has close, Has content; the two Buttons and the Close are exposed — set their labels and states on the instance
  • Content is a slot: replace it with the body (a Field, a Textarea, a list). Keep the footer’s primary as the verb: Publish, Delete, Send
  • Sits on Scrim; elevation/modal; radius 16

Use when the user must answer before going on — confirm, name, choose, send.

Do not use when the content is a whole task (a page or a Sheet), the message needs no answer (Alert, Toast), or the surface anchors to a trigger (Popover).

Relates to Scrim, Alert dialog, Sheet, Drawer, Button, Icon button - ghost, Field, Textarea.

Anti-patterns a dialog over a dialog · a dialog without a way out · three primaries in the footer · a title that is a sentence with a full stop · scrolling inside a small dialog.

Keywords: dialog, modal, confirm, popup, overlay

Scrim is the overlay; in code it is Dialog’s backdrop, not a component

Figma — page Dialog: Dialog (3 variants) · Scrim (1 variants)

shadcn: dialog

The class, the parts, and nothing that is not in the package. The same block opens dialog.css.

<div class="pui-scrim" data-state="open">
<div class="pui-dialog pui-dialog--medium" role="dialog" aria-modal="true" aria-labelledby="t">
<div class="pui-dialog__header"><div class="pui-dialog__texts"><h2 class="pui-dialog__title" id="t">Rename deck</h2>
<p class="pui-dialog__description">…</p></div>
<button class="pui-icon-button pui-icon-button--ghost pui-icon-button--small pui-dialog__close" aria-label="Close">…x…</button></div>
<div class="pui-dialog__content"> …a Field… </div>
<div class="pui-dialog__footer"><button class="pui-button pui-button--secondary">Cancel</button><button class="pui-button">Save</button></div></div></div>
What it is Note
Size (none) small 400 · --medium 480 · --large 680 —
Scrim .pui-scrim — fixed, the viewport, centred content; [hidden] when closed —

Every number here is measured on the rendered component: contrast (npm run contrast:render), the focus ring (npm run focus) and the keyboard (npm run keyboard). The floors are principle 12’s.

ink on ground reads floor the matrix calls it
#544d4d on #f2f2f7 7.39 4.5 text.muted on surface.card (promised) ok
#0b0202 on #f2f2f7 18.38 4.5 text.default on surface.card (promised) ok
#0b0202 on #ffffff 20.51 4.5 text.default on surface.page (promised) ok
#ffffff on #0b0202 20.51 4.5 Primary button label (promised) ok

4 distinct pair(s) rendered, 0 that axe calls a violation.

4 of 4 focus stop(s) in this block meet WCAG 2.4.13 on screen — the same crop screenshotted focused and unfocused, and the difference measured.

role tab stops what the pattern requires
dialog .pui-dialog.pui-dialog--medium 4 — —

What the consumer’s script owes. focus moves into the dialog on open and stays inside while it is open — legal under WCAG 2.1.2 ONLY because Escape gets out; Escape closes and focus returns to whatever opened it.

Tab order across the whole library is document order over 344 stops, and Shift+Tab is its exact reverse — no keyboard trap anywhere (WCAG 2.1.2).

Pepper UI v1.5.0

The Mobupps design system. 386 tokens, 88 components, 65 pages in the workshop — every page of this site generated from the repository, so nothing here is typed twice.