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