TanStack

Dialog

The centered modal. Always Radix-backed — focus trapping, focus restoration, Escape-to-dismiss and scroll lock come from the primitive, not from each call site. Composed from Dialog + DialogContent + DialogHeader / DialogBody / DialogFooter. Source: src/components/ds/ui/Dialog.tsx.

Basic

Header, body and footer are separate parts so a dialog can omit any of them.

Sizes

xs through xl. Every size keeps a 1rem viewport gutter, so the panel never touches the screen edge on mobile.

Scrolling body

The panel caps at calc(100dvh - 2rem) and the body scrolls. None of the six centered dialogs on the site do this today — the 5-step deploy wizard simply overflows the viewport.

Destructive confirm

Replaces the inline confirm in routes/admin/roles.$roleId.tsx, which had no focus trap, no Escape handler and no close button — a destructive action a keyboard user could not dismiss.

Tinted header

`media` puts a mark in a tile beside the title; `tint` colours that tile and washes the header behind it. It is a deliberate escape hatch from the token layer — these are third-party brand colours (Cloudflare orange, Netlify teal) that cannot be DS tokens because they are not ours. Everything else in the header stays on tokens.

Status panel

`DialogStatus` is the mark / heading / description / actions layout that every non-form step of the deploy dialogs already used. Five hand-built copies, each picking its own raw red or green Tailwind pair; tones now map onto the status-* tokens. Errors carry a role of alert and the loading tone is aria-live, so an outcome that only changes pixels is still announced.

Token coverage

Every visual attribute of the panel, and the DS token that supplies it. Three tokens did not exist before this component and were added to app.css.

AttributeTokenStatusNote
Panel surfacebg-background-elevatedexistingHighest of the three background tiers. Identical to `surface` in light mode; #2b2b2b in dark, where the shipping dialogs use the warm gray-900 (#201b15) off a different ramp.
Panel borderborder-border-defaultexistingCarries the edge in light mode, where all three background tiers are #ffffff and only shadow separates layers.
Elevationshadow-2xlexistingAlready the de facto modal elevation; 4 of 6 centered dialogs use it (one outlier at shadow-xl).
Corner radiusrounded-xl corner-squircleexistingrounded-xl is the audited majority. corner-squircle matches Card and Button.
Title / body texttext-text-primary, text-text-mutedexistingReplaces text-gray-900 dark:text-gray-100 at every call site.
Close affordancetext-icon-muted, hover:bg-surface-state-hoverexistingThe interaction-state overlay tokens already exist and were unused by every dialog.
Focus ringring-border-focusexistingfocus-visible only, so the ring does not appear on mouse click.
Scrimbg-scrimaddedDid not exist. Seven hand-picked black/NN values across the audit. Deliberately heavier in dark (0.65 vs 0.5) — equal alpha reads as weaker separation over an already-dark page.
Stacking tierz-[var(--z-scrim)] / z-[var(--z-overlay)]addedDid not exist. Five unrelated z-index families were in use. Values set to 999/1000 — the existing majority — so adopting them moves nothing. A third tier, --z-above-overlay (1200), covers chrome that must float over an open overlay, such as a tooltip on a control inside a modal.
Motionanimate-dialog-panel-in / -outaddedKeyframes are real. The animate-in / fade-in-0 / zoom-in-95 classes used elsewhere in the codebase come from tailwindcss-animate, which is not installed — they match zero CSS rules. Timing reuses --motion-duration-fast and --motion-ease-standard.

Behaviour from Radix

Not configurable, by design. Every accessibility failure in the overlay audit came from a hand-rolled dialog missing one of these.

  • Focus moves into the panel on open and returns to the trigger on close.
  • Tab and Shift+Tab are trapped inside the panel.
  • Escape dismisses. Clicking the scrim dismisses.
  • Body scroll is locked while open.
  • aria-modal, role="dialog", and the title/description associations are wired automatically.
  • prefers-reduced-motion removes the animation but keeps the state change.