Installation
The registry is served as JSON. Add it with the shadcn CLI — the files land in components/ui/sapa-toast/.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toaster.jsonMount the Toaster
Render <Toaster /> once, near the root of your app. After that you can call toast() from anywhere.
import { Toaster } from "@/components/ui/sapa-toast/toaster"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
<Toaster position="bottom-right" />
</body>
</html>
)
}Basic usage
Call toast() with a message. Everything else is optional.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-default.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
export default function ToastDefault() {
return (
<button
type="button"
onClick={() => toast("Event has been created")}
className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
>
Show toast
</button>
);
}
Loading & dismiss
toast.loading() persists until you dismiss or replace it. Every call returns an id you can pass to toast.dismiss().
import { toast } from "@/components/ui/sapa-toast/toaster"
const id = toast.loading("Saving…") // persists until you change it
toast.dismiss(id) // dismiss one
toast.dismiss() // dismiss every toastTypes & rich colors
Success, error, warning and info — each with a semantic color + icon.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-types.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
const btn =
"inline-flex h-9 items-center rounded-md border bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-muted";
export default function ToastTypes() {
return (
<div className="flex flex-wrap gap-2">
<button
type="button"
className={btn}
onClick={() => toast.success("Changes saved")}
>
Success
</button>
<button
type="button"
className={btn}
onClick={() => toast.error("Something went wrong")}
>
Error
</button>
<button
type="button"
className={btn}
onClick={() => toast.warning("Your session is about to expire")}
>
Warning
</button>
<button
type="button"
className={btn}
onClick={() => toast.info("A new update is available")}
>
Info
</button>
</div>
);
}
Title + description
A headline with a secondary line of detail.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-description.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
export default function ToastDescription() {
return (
<button
type="button"
onClick={() =>
toast.success("Event created", {
description: "Monday, January 3rd at 6:00pm",
})
}
className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
>
With description
</button>
);
}
Action + cancel
Inline buttons, e.g. undo a destructive change.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-action.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
export default function ToastAction() {
return (
<button
type="button"
onClick={() =>
toast("File deleted", {
description: "report-2026.pdf was moved to trash.",
duration: 8000,
action: {
label: "Undo",
onClick: () => toast.success("Restored"),
},
cancel: { label: "Dismiss" },
})
}
className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
>
With action
</button>
);
}
Promise
Drive one toast through loading → success/error.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-promise.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
function fakeUpload(): Promise<{ name: string }> {
return new Promise((resolve, reject) =>
setTimeout(() => {
// Randomly resolve/reject to show both branches.
if (Math.round(Date.now() / 1000) % 2 === 0) {
resolve({ name: "avatar.png" });
} else {
reject(new Error("Network error"));
}
}, 1800),
);
}
export default function ToastPromise() {
return (
<button
type="button"
onClick={() =>
toast.promise(fakeUpload(), {
loading: "Uploading file…",
success: (data) => `${data.name} uploaded`,
error: (err) => `Upload failed: ${(err as Error).message}`,
})
}
className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
>
Promise
</button>
);
}
Progress
A determinate progress bar — update it in place, then finish as success.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-progress.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
function startUpload() {
let value = 0;
const id = toast.progress("Uploading avatar.png…", {
value,
description: "0%",
});
const timer = setInterval(() => {
value = Math.min(100, value + Math.round(8 + Math.random() * 14));
toast.progress("Uploading avatar.png…", {
id,
value,
description: `${value}%`,
});
if (value >= 100) {
clearInterval(timer);
// Transition the same toast in place once the work is done —
// clearing `progress` removes the bar from the success toast.
toast.success("avatar.png uploaded", {
id,
description: undefined,
progress: undefined,
});
}
}, 350);
}
export default function ToastProgress() {
return (
<button
type="button"
onClick={startUpload}
className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
>
Upload file
</button>
);
}
Custom content
Render any markup — JSX in React, a component in Vue.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-custom.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
export default function ToastCustom() {
return (
<button
type="button"
onClick={() =>
toast.custom(
<div className="flex items-center gap-3">
<img
src="https://avatars.githubusercontent.com/u/124599?v=4"
alt=""
className="size-10 rounded-full"
/>
<div className="space-y-0.5">
<p className="text-sm font-medium">Olivia Martin</p>
<p className="text-muted-foreground text-xs">
sent you a friend request
</p>
</div>
<button
type="button"
className="bg-foreground text-background ml-2 inline-flex h-7 items-center rounded-md px-2.5 text-xs font-medium"
onClick={() => toast.success("Accepted")}
>
Accept
</button>
</div>,
{ duration: 6000 },
)
}
className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
>
Custom content
</button>
);
}
Positions
Six anchor positions, set per-toast or globally.
top-lefttop-centertop-rightbottom-leftbottom-centerbottom-rightnpx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-positions.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
import type { ToastPosition } from "@/components/ui/sapa-toast/toast-store";
const POSITIONS: ToastPosition[] = [
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
];
export default function ToastPositions() {
return (
<div className="grid grid-cols-3 gap-2">
{POSITIONS.map((position) => (
<button
key={position}
type="button"
onClick={() => toast(position, { position, type: "info" })}
className="bg-background hover:bg-muted inline-flex h-9 items-center justify-center rounded-md border px-3 text-xs font-medium shadow-sm transition-colors"
>
{position}
</button>
))}
</div>
);
}
Stack
Toasts collapse into a pile; hover to expand.
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-stack.json"use client";
import { toast } from "@/components/ui/sapa-toast/toaster";
const MESSAGES = [
"Event has been created",
"Your report is ready",
"New comment on your post",
"Payment received",
"Deployment finished",
] as const;
export default function ToastStack() {
return (
<button
type="button"
onClick={() => {
MESSAGES.forEach((m, i) =>
setTimeout(() => toast(m, { description: "Just now" }), i * 220),
);
}}
className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
>
Stack toasts
</button>
);
}
Options
The second argument of every toast() call. Identical in React and Vue.
| Prop | Type | Description |
|---|---|---|
| title | ReactNode | Main message — the first argument of toast(). |
| description | ReactNode | Secondary line of detail below the title. |
| duration | number | Auto-dismiss delay in ms (default 4000). Use Infinity to persist. |
| position | ToastPosition | One of the six anchors — overrides the Toaster default. |
| icon | ReactNode | Custom leading icon; falls back to the type's default. |
| progress | number | 0–100 — a determinate bar along the bottom edge (see toast.progress). |
| action | { label, onClick } | Primary inline button, e.g. Undo. |
| cancel | { label, onClick } | Secondary / dismiss button. |
| id | string | number | Stable id — reuse it to update a toast in place. |
| onDismiss | (toast) => void | Called once the toast is removed, whatever closed it. |
| onAutoClose | (toast) => void | Called only when the duration ran out — not on close, swipe or action. |
<Toaster> props
Defaults for the mounted toaster.
| Prop | Type | Description |
|---|---|---|
| position | ToastPosition | Default position for toasts that don't set their own (default "bottom-right"). |
| expand | boolean | Show toasts as a flat list instead of a collapsible stack (default false). |
Custom colors
The rich colors come from four semantic CSS variables — --sapa-success, --sapa-error, --sapa-warning and --sapa-info. The toast's typepicks one, and it does two jobs: it tints the icon tile — a soft wash, a hairline of the same hue, and the glyph — and it paints the bar along the card's bottom edge, whether that bar is counting down the auto-dismiss or tracking a progress value. The theme registry item installs them into your project on setup; to re-brand the toast, just override them in your own globals.css — no component changes needed.
| Token | What it colors |
|---|---|
| --sapa-success | Success tile and bottom-edge bar. |
| --sapa-error | Error tile and bottom-edge bar. |
| --sapa-warning | Warning tile and bottom-edge bar. |
| --sapa-info | Info tile and bottom-edge bar. |
Redefine any of them under :root (light) and .dark (dark). Values use oklch, but any valid CSS color works.
/* globals.css — override Sapa's rich colors */
:root {
--sapa-success: oklch(0.62 0.17 149);
--sapa-error: oklch(0.58 0.22 27);
--sapa-warning: oklch(0.75 0.16 78);
--sapa-info: oklch(0.6 0.13 240);
}
.dark {
--sapa-success: oklch(0.7 0.16 150);
--sapa-error: oklch(0.7 0.19 22);
--sapa-warning: oklch(0.82 0.16 82);
--sapa-info: oklch(0.68 0.14 240);
}The result — flip the theme to see both palettes: