A modal that gives a focused subtask its own surface — settings, detail views, embedded forms, sign-in flows. Escape and backdrop dismiss it. For destructive or irreversible operations where the user must explicitly choose, reach for Alert Dialog instead.
Dialog is a compound — Root owns open state, Trigger opens it, Content is the panel (auto-portaled with its own overlay), Header stacks the Title and Description, Footer lays out trailing actions, and Close dismisses the dialog from any element inside.
import { Dialog } from "@craft-bits/core";<Dialog.Root> <Dialog.Trigger asChild> <button>Edit profile</button> </Dialog.Trigger> <Dialog.Content> <Dialog.Header> <Dialog.Title>Edit profile</Dialog.Title> <Dialog.Description> Update your display name and avatar. Changes save instantly. </Dialog.Description> </Dialog.Header> <Dialog.Footer> <Dialog.Close>Done</Dialog.Close> </Dialog.Footer> </Dialog.Content></Dialog.Root>
Drive the open state from your own store by passing open plus onOpenChange:
Dialog vs. AlertDialog. A Dialog is informational — Escape and the backdrop dismiss it freely. AlertDialog is for blocking confirmations: Escape is swallowed, initial focus lands on Cancel, and the only way out is an explicit choice. Pick Dialog for non-destructive flows (edit, preview, sign in) and AlertDialog for destructive ones (delete, sign out, discard).
Compound parts. Root (state owner), Trigger (opens the dialog, accepts asChild), Overlay (backdrop scrim), Content (the panel; auto-wraps in Portal + Overlay unless you pass withPortal={false}), Header and Footer (layout helpers), Title and Description (Radix-wired aria-labelledby and aria-describedby), Close (imperative close button, accepts asChild).
Controlled or uncontrolled. Omit open and onOpenChange to let Radix manage state. Pass both to drive open/close from your own state — useful for triggering the dialog from a remote action (right-click menu, keyboard shortcut, async callback).
Title and Description are required. Radix-style dialogs must label themselves for assistive tech. Visually hide them with sr-only if your design has none — never omit them.
Close button placement. Footer reverses the column order on mobile so the primary action sits on top (thumb-first), then switches to a right-aligned row on sm: so it ends up on the right. Drop a styled close trigger anywhere inside Content — top-right corner, footer, or both.
asChild everywhere. Trigger and Close accept asChild — drop in your own Button primitive and the dialog's event handlers merge onto it via Radix Slot.
Props
Dialog.Root
Prop
Type
Default
Description
open
boolean
—
Controlled open state. Pair with onOpenChange.
onOpenChange
(open: boolean) => void
—
Fires when the dialog opens or closes.
defaultOpen
boolean
false
Uncontrolled initial open state.
modal
boolean
true
When false, interaction outside the dialog stays enabled.
Dialog.Trigger
Prop
Type
Default
Description
asChild
boolean
false
Render trigger props onto the child element via Radix Slot.
className
string
—
Merged onto the rendered element.
Dialog.Content
Prop
Type
Default
Description
withPortal
boolean
true
Auto-wrap in Portal + Overlay. Set false to compose them yourself.
overlayClassName
string
—
Merged onto the auto-rendered overlay (only when withPortal is true).
className
string
—
Merged onto the rendered panel.
Dialog.Header / Dialog.Footer
Prop
Type
Default
Description
className
string
—
Merged onto the rendered <div>.
Dialog.Title / Dialog.Description
Prop
Type
Default
Description
className
string
—
Merged onto the rendered element.
Dialog.Close
Prop
Type
Default
Description
asChild
boolean
false
Render close props onto the child element via Radix Slot.
className
string
—
Merged onto the rendered button.
Accessibility
Radix renders the panel as role="dialog" — assistive tech announces it as a modal subtask.
Title is wired as the panel's accessible name (aria-labelledby); Description is wired as its description (aria-describedby). Both are required — wrap them in sr-only if your visual design has none.
Tab traps focus inside the panel until it closes.
Escape closes the dialog. Clicking the backdrop closes the dialog. If you need to require a deliberate choice, use Alert Dialog instead.
The trigger receives focus back when the dialog closes so keyboard users return to where they were.
Credits
Extracted from: algoflashcards (src/platform/ui/dialog.tsx). The original embedded the project's Button primitive into the close affordance and hardcoded a phosphor XIcon; craft-bits strips that coupling, exposes a headless Dialog.Close that consumers wire to their own button (via asChild), and rewires the styling to cb-* tokens.