Toast
FreeA short, dismissible message that confirms an action, with an optional undo.
pnpm dlx shadcn@latest add https://caliperui.site/r/toast.jsonWhen to use #
Use it for
- Confirming something the person just did, such as archiving a project.
- Offering Undo for a reversible action.
Skip it when
- The message must be read before continuing; use an alert or dialog.
- The content is long; keep toasts to a title and one line.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Loading resolves in place
- A toast that starts as “Saving…” becomes “Saved” or the error where it is, instead of stacking a second toast.
- Errors and loading never time out
- A message that needs action or is still working stays until it is dealt with; only plain confirmations leave on their own.
Styling
- Frosted, because it floats
- Toasts use the shared frosted surface with a 14px radius; the reading canvas underneath stays solid.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/toast.json
Adds its 4 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 Toast, ToastProvider and ToastStack.
import { useState } from "react" import { Button } from "@/components/ui/button" import { Toast, ToastProvider, ToastStack } from "@/components/ui/toast" export function ArchiveToast() { const [sending, setSending] = useState<"idle" | "pending" | "done">("idle"); const [failed, setFailed] = useState(false); return ( <ToastProvider duration={5000}> <div className="flex flex-wrap justify-center gap-2"> <Button onClick={() => { setSending("pending"); window.setTimeout(() => setSending("done"), 1600); }} > Send 3 invoices </Button> <Button variant="secondary" className="text-danger" onClick={() => setFailed(true)}>Charge failing card</Button> </div> <Toast title={sending === "pending" ? "Sending 3 invoices…" : "Sent 3 invoices"} loading={sending === "pending"} variant="success" open={sending !== "idle"} onOpenChange={(open) => !open && setSending("idle")} > {sending === "pending" ? "Ostra Freight, Halden Analytics, Lumen Labs" : "Each customer got a PDF and a pay link."} </Toast> <Toast title="Lumen Labs card declined" variant="error" open={failed} onOpenChange={setFailed} action={{ label: "Email Lumen Labs", altText: "Ask Lumen Labs to update their card", onClick: () => setFailed(false) }} > Insufficient funds. We will retry on 12 Oct. Errors stay until you dismiss them. </Toast> <ToastStack /> </ToastProvider> ); }
API reference #
Toast
component
Also accepts the native props of its root element and a ref.
ToastProvider
component
No props of its own; it accepts the native props of its root element.
ToastStack
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-busy, aria-labelledby, aria-describedby, aria-label, aria-hidden and aria-live in step with state. Changes that matter are announced through a live region.
Keyboard and focus handling comes from Radix toast: arrow keys move through items, Enter and Space select, and Escape closes and returns focus.
Motion #
Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.