caliper uidocs

Dialog

Free

A modal window that asks for a decision or a short form, with focus kept inside.

Installs components/ui/dialog.tsxDepends on @radix-ui/react-dialog, @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/dialog.json

3 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 #

terminal
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.

example.tsx
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.

PropTypeDefault
showCloseButtonbooleantrue

DialogHeader

component

No props of its own; it accepts the native props of its root element.

DialogFooter

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.