Skip to main content

Dialog

betav0.1.0

Modal overlay that interrupts the current context to request confirmation, collect input, or surface focused content. Built on @radix-ui/react-dialog for accessible behavior (focus trap, Escape/backdrop dismiss, focus return to trigger). Compound component: Dialog > DialogTrigger + DialogContent > DialogTitle + DialogDescription? + DialogBody + DialogFooter. Scroll is internal to DialogBody — the title, description, and footer stay fixed while the body scrolls.

Variants

size="sm" — destructive confirmation

Examples

<Dialog>
  <DialogTrigger asChild>
    <Button priority="primary">Delete indicator</Button>
  </DialogTrigger>
  <DialogContent size="sm">
    <DialogTitle>Delete this indicator?</DialogTitle>
    <DialogDescription>This action cannot be undone.</DialogDescription>
    <DialogFooter>
      <DialogClose asChild>
        <Button priority="secondary">Cancel</Button>
      </DialogClose>
      <Button variant="destructive" priority="primary">Delete</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
// size defaults to "md" — long or dynamic content goes in DialogBody so it
// scrolls internally while the title/footer stay fixed (arbitration #10)
<Dialog>
  <DialogTrigger asChild>
    <Button priority="primary">Create label</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogTitle>Create label</DialogTitle>
    <DialogDescription>Labels help you organize and filter entities.</DialogDescription>
    <DialogBody>
      {/* form fields, lists, or any long-form content */}
    </DialogBody>
    <DialogFooter>
      <DialogClose asChild>
        <Button priority="secondary">Cancel</Button>
      </DialogClose>
      <Button priority="primary">Save</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
// DialogDescription is optional — omitting it is valid, not a contract
// violation (only a missing DialogTitle triggers FDS's dev warning)
<DialogContent size="lg" hideCloseButton>
  <DialogTitle>Import STIX bundle</DialogTitle>
  <DialogBody>…</DialogBody>
  <DialogFooter>
    <DialogClose asChild>
      <Button priority="secondary">Back</Button>
    </DialogClose>
    <Button priority="primary">Continue</Button>
  </DialogFooter>
</DialogContent>

Props

PropType
Dialog.openboolean — Controlled open state (Radix Root prop)
Dialog.onOpenChange(open: boolean) => void — Fires on any open-state change (backdrop, Escape, trigger, close)
Dialog.defaultOpenboolean — Uncontrolled initial open state (default: false)
DialogContent.size"sm" | "md" | "lg" — Panel width, matches Figma's 3 designed sizes (default: "md")
DialogContent.hideCloseButtonboolean — Hides the built-in corner close icon (default: false — visible, conforms to Figma)
DialogTitle.childrenReactNode — Required for accessibility (dialog name, WCAG 2.4.6/4.1.2). FDS warns in dev if omitted.
DialogDescription.childrenReactNode — Optional short subtitle. Not for arbitrary body content — use DialogBody for that.
DialogBody.childrenReactNode — The scrollable middle region. Title/Description/Footer stay fixed; only DialogBody scrolls when content overflows.
DialogFooter.childrenReactNode — Action row, typically Button instances (Cancel + primary/destructive action).
DialogTrigger.asChild / DialogClose.asChildboolean — Radix convention: render the single child element directly instead of wrapping it (e.g. to use a Button as the trigger).

Accessibility

2.1 AAWCAG pass

RFC §5.4. The overlay/backdrop itself (--bg-elevation-default-layer-0 at 80% opacity + 4px blur, --blur-sm, landed PR #49 — superseded from the originally-arbitrated 2px, see RFC §9 arbitration #21) is intentionally NOT a contrastPairs entry — it's a non-text dimming scrim, not a text/UI-component contrast requirement (RFC §5.4 final row, 'n/a — not a text contrast pair'), consistent with how decorative overlays are treated across WCAG 1.4.3/1.4.11. 'Dialog panel text (overlay-composited context)' duplicates the title/content pair deliberately, matching NavbarSubmenu.meta.ts's precedent of listing the same token pair once per distinct rendering context — DialogContent's own background is fully opaque, so the ratio is identical to the plain-panel entries; the separate entry documents that the pairing was verified in that context too, not just assumed. visual-fix-pass-1 (background fix): DialogContent now renders inside the pre-existing `.layer-2` context class (theme.css) so that its `bg-elevation-default` utility resolves to #13213e/--gray-50 (dark/light) instead of the un-layered default #070d18/#f2f2f3 — see Dialog.tsx for the full reasoning. All contrastPairs `bg` fields above intentionally stay literal `--bg-elevation-default` (not `-layer-2`) because that's the actual CSS custom property the rendered class resolves through, and check-contrast.ts's Rule 5 coverage check matches contrastPairs by that literal name, with no notion of the `.layer-2` class's cascade — a known blind spot of the static checker, not something this file can route around without breaking Rule 5 coverage. Concretely this means the ratios `pnpm check:contrast` computes/reports use the AMBIENT (layer-0) color, not the real rendered (layer-2) one. Hand-verified with the exact same WCAG 2.x formula (theme.ts's relativeLuminance/contrastRatio) against the real layer-2 colors so this isn't just asserted: title/content/close-icon-bg/Cancel-bg/overlay-context pairs are dark 14.28:1 (real, layer-2 #13213e) vs 17.38:1 (ambient #070d18 as gate-reported) for the text-default-primary pairs (≥4.5 either way), and light 16.02:1 (real, layer-2 #f4f4f6) vs 15.72:1 (ambient #f2f2f3 as gate-reported, also ≥4.5); the brand-primary-on-panel pairs (close icon, Cancel) are dark 7.35:1 (real) vs 8.94:1 (ambient, ≥3 and ≥4.5 either way) and light 11.43:1 (real) vs 11.22:1 (ambient, same). No WCAG risk either way — this is a reporting-precision note, not a compliance gap. Keyboard: focus trapped inside the panel while open (Tab/Shift+Tab cycle); Escape closes and returns focus to DialogTrigger; backdrop click closes (Radix defaults, no deviation — arbitration #7/#8). role="dialog" aria-modal="true", aria-labelledby wired to DialogTitle automatically by Radix; aria-describedby wired to DialogDescription when present, silently omitted when absent (verified empirically against the installed @radix-ui/react-dialog@1.1.22 — no warning either way, §5.3).