Dialog
A modal surface that interrupts the flow for a focused task or message.
@garn/ui/dialogDefault
Trigger → titled dialog with footer actions.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add dialog- Registry
dialog- Source
- dialog.tsx
- Deps
@radix-ui/react-dialoglucide-react- Registry deps
utils
Import
import { Dialog } from "@garn/ui/dialog";Props
Generated from the component's TypeScript types.
No component-specific props — this is a thin wrapper over its native element.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="dialog-content"]| Property | Token | Tier |
|---|---|---|
padding | --garn-pad-surface | semantic · air |
gap | --garn-gap-stack | semantic · air |
closeSize | --garn-control-h-xs | semantic · air |
motion | --garn-motion-fast | semantic |
Overview
Dialog interrupts the flow for a focused task that needs attention before the user moves on — a short form, a detail view, a confirmation. It's a compound built on Radix: Dialog owns the open state, DialogTrigger opens it, and DialogContent is a focus-trapped surface that renders its own overlay and close button. Inside, DialogTitle (required), DialogDescription, and the DialogHeader/DialogFooter layout wrappers structure the content; DialogClose dismisses it.
The modal contract is the whole point: focus is trapped while open and returned to the trigger on close, the background is inert, and Escape closes. Pick Dialog when the task is short and self-contained. For a destructive confirmation that must not be dismissed by clicking away, use AlertDialog; for a long form or a side panel, use Sheet; for lightweight, non-blocking context anchored to a control, use Popover.
Guidelines
Always render a DialogTitle — it's the dialog's accessible name and the contract is enforced. If the design has no visible heading, keep the title and visually hide it (sr-only); don't drop it. Pair it with a DialogDescription whenever a sentence of context would help, so it wires to aria-describedby.
Keep dialogs short and single-purpose. A dialog is for one focused task; if it grows into a multi-step flow or a long scroll, that's a sign it should be a full page or a Sheet. Put the primary action in the DialogFooter as the one filled button, with Cancel/Close demoted beside it.
Don't stack or auto-open dialogs. Opening a dialog from inside a dialog traps focus in a confusing nest; resolve one before opening the next. Reserve dialogs for moments that genuinely warrant interrupting the user — overusing them for messages that a Sonner toast or inline Alert could carry trains people to dismiss them on reflex.
Best practices
- Always include a DialogTitle (sr-only if you don't want it visible).
- Keep the task short; put primary/secondary actions in a DialogFooter.
- Let Escape and the overlay close it — don't trap users.
- Don't stack dialogs or nest a dialog inside a dialog.
- Don't use a modal for a destructive confirm — alert-dialog is purpose-built for that.
Content guidelines
- Give the dialog a specific title that names the task (“Invite teammate”).
- Label the primary action by outcome and keep the dismiss action clearly secondary.
Accessibility
- Role
dialog- ARIA APG
- dialog (modal)
- Focus
- Focus trapped inside the content; returns to the trigger on close.
Enforceable contracts
Every DialogContent must contain a DialogTitle (wrap it in sr-only to hide it visually).
when DialogContent has no DialogTitle
Keyboard
| Escape | Close the dialog. |
| TabShift+Tab | Cycle focus within the dialog (focus is trapped). |
- • aria-modal; labelled by DialogTitle and (if present) described by DialogDescription.
- • Focus moves into the dialog on open and returns to the trigger on close.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.