Dialog

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.

Installation

npx shadcn@latest add https://craftbits.dev/r/dialog.json

Usage

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:

const [open, setOpen] = useState(false);
 
<Dialog.Root open={open} onOpenChange={setOpen}>
  {/* ... */}
</Dialog.Root>

Understanding the component

  1. 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).
  2. 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).
  3. 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).
  4. 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.
  5. 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.
  6. 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

PropTypeDefaultDescription
openbooleanControlled open state. Pair with onOpenChange.
onOpenChange(open: boolean) => voidFires when the dialog opens or closes.
defaultOpenbooleanfalseUncontrolled initial open state.
modalbooleantrueWhen false, interaction outside the dialog stays enabled.

Dialog.Trigger

PropTypeDefaultDescription
asChildbooleanfalseRender trigger props onto the child element via Radix Slot.
classNamestringMerged onto the rendered element.

Dialog.Content

PropTypeDefaultDescription
withPortalbooleantrueAuto-wrap in Portal + Overlay. Set false to compose them yourself.
overlayClassNamestringMerged onto the auto-rendered overlay (only when withPortal is true).
classNamestringMerged onto the rendered panel.

Dialog.Header / Dialog.Footer

PropTypeDefaultDescription
classNamestringMerged onto the rendered <div>.

Dialog.Title / Dialog.Description

PropTypeDefaultDescription
classNamestringMerged onto the rendered element.

Dialog.Close

PropTypeDefaultDescription
asChildbooleanfalseRender close props onto the child element via Radix Slot.
classNamestringMerged 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.
  • Built on: @radix-ui/react-dialog.