Dialog
FreeA modal window that asks for a decision or a short form, with focus kept inside.
pnpm dlx shadcn@latest add https://caliperui.site/r/dialog.json3 selected · 11 remain
When to use #
Use it for
- Confirming something consequential or collecting a few fields.
- The task must be finished or cancelled before continuing.
Skip it when
- The content is a quick, optional detail; use a popover.
- Phones and long flows; use a bottom sheet or drawer.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- A busy confirm keeps the dialog open
- While the action runs the dialog stays, so a failure can be shown where the decision was made. Focus returns to whatever opened it when it closes.
- The footer is pinned
- With a long body the actions stay in view behind a divider, so the decision is always one press away.
Styling
- A sheet on phones
- Under 640px the dialog rises from the bottom edge with stacked 44px actions, where thumbs reach.
Usage
- The title states the decision with real numbers
- “Remove 3 seats? You will be credited €72” beats “Are you sure?”. The confirming button repeats the action, never just OK.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/dialog.json
Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from Dialog, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle and DialogDescription.
import { useState } from "react" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" export function InviteDialog() { const [keys, setKeys] = useState(true); const [busy, setBusy] = useState(false); const [open, setOpen] = useState(false); const [result, setResult] = useState("3 selected · 11 remain"); return ( <div className="grid justify-items-center gap-3"> <Dialog open={open} onOpenChange={(next) => !busy && setOpen(next)}> <DialogTrigger asChild> <Button variant="secondary" className="text-danger">Remove 3 members</Button> </DialogTrigger> <DialogContent role="alertdialog"> <DialogHeader> <DialogTitle>Remove 3 members from Halden Analytics?</DialogTitle> <DialogDescription> They lose access right away. Their dashboards and comments stay. Your bill drops by <span className="font-medium text-foreground tabular-nums">$18 per month</span> from 1 November. </DialogDescription> </DialogHeader> <ul className="grid gap-1 text-sm"> <li>Tomás Ferreira</li> <li>Jun Lee</li> <li>Amara Bello</li> </ul> <label className="flex items-center gap-3 text-sm"> <Checkbox checked={keys} onChange={(event) => setKeys(event.currentTarget.checked)} /> Also revoke their 4 API keys </label> <DialogFooter> <DialogClose asChild><Button variant="secondary" disabled={busy}>Cancel</Button></DialogClose> <Button variant="destructive" loading={busy} onClick={() => { setBusy(true); window.setTimeout(() => { setBusy(false); setOpen(false); setResult(`Removed Tomás Ferreira, Jun Lee and Amara Bello${keys ? " and revoked 4 API keys" : ""}.`); }, 900); }} > Remove 3 members </Button> </DialogFooter> </DialogContent> </Dialog> <p role="status" className="max-w-sm text-center text-sm text-muted-foreground">{result}</p> </div> ); }
API reference #
Dialog
component
No props of its own; it accepts the native props of its root element.
DialogTrigger
component
No props of its own; it accepts the native props of its root element.
DialogClose
component
No props of its own; it accepts the native props of its root element.
DialogContent
component
Also accepts the native props of its root element and a ref.
DialogHeader
component
No props of its own; it accepts the native props of its root element.
DialogTitle
component
No props of its own; it accepts the native props of its root element.
DialogDescription
component
No props of its own; it accepts the native props of its root element.
Accessibility #
It is built on native elements and keeps aria-label and aria-hidden in step with state.
Keyboard and focus handling comes from Radix dialog: arrow keys move through items, Enter and Space select, and Escape closes and returns focus.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.