Toast
Generates toast notifications. Built with Base UI and Tailwind CSS. Copy-paste ready.
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<Button
onClick={() => {
toastManager.add({
description: "Monday, January 3rd at 6:00pm",
title: "Event has been created",
});
}}
variant="outline"
>
Default Toast
</Button>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/toast
Add the ToastProvider and AnchoredToastProvider to your app.
import { AnchoredToastProvider, ToastProvider } from "@/components/ui/toast"
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<ToastProvider>
<AnchoredToastProvider>
<main>{children}</main>
</AnchoredToastProvider>
</ToastProvider>
</body>
</html>
)
}Usage
Stacked Toasts
import { toastManager } from "@/components/ui/toast"toastManager.add({
title: "Event has been created",
description: "Monday, January 3rd at 6:00pm",
})By default, toasts appear in the bottom-right corner. You can change this by setting the position prop on the ToastProvider.
Allowed values: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right. For example:
<ToastProvider position="top-center">{children}</ToastProvider>Anchored Toasts
For toasts positioned relative to a specific element, use anchoredToastManager. The AnchoredToastProvider is typically added to your app layout (alongside ToastProvider), so you can use anchoredToastManager directly in your components:
anchoredToastManager.add({
title: "Copied!",
positionerProps: {
anchor: buttonRef.current,
},
})You can also style anchored toasts like tooltips by passing data: { tooltipStyle: true }. When using tooltip style, only the title is displayed (description and other content are ignored):
anchoredToastManager.add({
title: "Copied!",
positionerProps: {
anchor: buttonRef.current,
},
data: {
tooltipStyle: true,
},
})Examples
With Status
Pass a status of "success", "error", "warning", or "info" to render a colored icon and tinted background alongside the message.
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<div className="flex flex-wrap gap-2">
<Button
onClick={() => {
toastManager.add({
description: "Your changes have been saved.",
title: "Success!",
type: "success",
});
}}
variant="outline"
>
Success Toast
</Button>
<Button
onClick={() => {
toastManager.add({
description: "There was a problem with your request.",
title: "Uh oh! Something went wrong.",
type: "error",
});
}}
variant="outline"
>
Error Toast
</Button>
<Button
onClick={() => {
toastManager.add({
description: "You can add components to your app using the cli.",
title: "Heads up!",
type: "info",
});
}}
variant="outline"
>
Info Toast
</Button>
<Button
onClick={() => {
toastManager.add({
description: "Your session is about to expire.",
title: "Warning!",
type: "warning",
});
}}
variant="outline"
>
Warning Toast
</Button>
</div>
);
}
Loading
A toast that starts with a loading spinner and transitions to success or error once the async operation resolves.
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<Button
onClick={() => {
toastManager.add({
description: "Please wait while we process your request.",
title: "Loading…",
type: "loading",
});
}}
variant="outline"
>
Loading Toast
</Button>
);
}
With Action
Includes an inline action button — commonly used for undo operations or quick-follow-up actions.
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<Button
onClick={() => {
const id = toastManager.add({
actionProps: {
children: "Undo",
onClick: () => {
toastManager.close(id);
toastManager.add({
description: "The action has been reverted.",
title: "Action undone",
type: "info",
});
},
},
description: "You can undo this action.",
timeout: 1000000,
title: "Action performed",
type: "success",
});
}}
variant="outline"
>
Perform Action
</Button>
);
}
Promise
Pass a Promise to toastManager.promise to automatically show loading, success, and error states as the promise resolves or rejects.
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<Button
onClick={() => {
toastManager.promise(
new Promise<string>((resolve, reject) => {
const shouldSucceed = Math.random() > 0.3;
setTimeout(() => {
if (shouldSucceed) {
resolve("Data loaded successfully");
} else {
reject(new Error("Failed to load data"));
}
}, 2000);
}),
{
error: () => ({
description: "Please try again.",
title: "Something went wrong",
}),
loading: {
description: "The promise is loading.",
title: "Loading…",
},
success: (data: string) => ({
description: `Success: ${data}`,
title: "This is a success toast!",
}),
},
);
}}
variant="outline"
>
Run Promise
</Button>
);
}
With Varying Heights
Demonstrates how multiple stacked toasts of different content lengths collapse and expand smoothly in the stack.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
const TEXTS = [
"Short message.",
"A bit longer message that spans two lines.",
"This is a longer description that intentionally takes more vertical space to demonstrate stacking with varying heights.",
"An even longer description that should span multiple lines so we can verify the clamped collapsed height and smooth expansion animation when hovering or focusing the viewport.",
];
export default function Particle() {
const [count, setCount] = useState(0);
function createToast() {
setCount((prev) => prev + 1);
const description = TEXTS[Math.floor(Math.random() * TEXTS.length)];
toastManager.add({
description,
title: `Toast ${count + 1} created`,
});
}
return (
<Button onClick={createToast} variant="outline">
With Varying Heights
</Button>
);
}
Form Submit
A form submit button that shows a success toast on completion and an error toast if the operation fails, without a page reload.
"use client";
import { useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { anchoredToastManager } from "@/components/ui/toast";
export default function Particle() {
const submitRef = useRef<HTMLButtonElement>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const toastIdRef = useRef<string | null>(null);
function handleSubmit() {
if (!submitRef.current || isSubmitting) return;
if (toastIdRef.current) {
anchoredToastManager.close(toastIdRef.current);
toastIdRef.current = null;
}
setIsSubmitting(true);
new Promise<void>((_, reject) => {
setTimeout(() => {
setIsSubmitting(false);
reject(
new Error("The server is not responding. Please try again later."),
);
}, 2000);
}).catch((error: Error) => {
toastIdRef.current = anchoredToastManager.add({
description: error.message,
positionerProps: {
anchor: submitRef.current,
sideOffset: 4,
},
title: "Error submitting form",
type: "error",
});
});
}
return (
<Button
disabled={isSubmitting}
onClick={handleSubmit}
ref={submitRef}
variant="outline"
>
{isSubmitting ? (
<>
<Spinner />
Submitting…
</>
) : (
"Submit"
)}
</Button>
);
}
Staggered Burst
Four toasts fire with staggered delays (0 / 800 / 1600 / 2400 ms) so they animate into the stack one after another. The trigger button is disabled while the burst is in progress.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
const notifications = [
{
delay: 0,
description: "Alex Rivera started following you.",
title: "New follower",
type: "info" as const,
},
{
delay: 800,
description: 'Your post "Building with Base UI" got 142 likes.',
title: "Post liked",
type: "success" as const,
},
{
delay: 1600,
description: 'Sarah replied to your comment: "Great point!"',
title: "New reply",
type: "info" as const,
},
{
delay: 2400,
description: "You were mentioned in a thread by @devmark.",
title: "Mentioned",
type: "warning" as const,
},
];
export function Pattern() {
const [firing, setFiring] = useState(false);
function fireAll() {
if (firing) return;
setFiring(true);
notifications.forEach(({ delay, ...toast }) => {
setTimeout(() => toastManager.add(toast), delay);
});
setTimeout(
() => setFiring(false),
(notifications.at(-1)?.delay ?? 0) + 500,
);
}
return (
<Button disabled={firing} onClick={fireAll} variant="outline">
{firing ? "Incoming…" : "Simulate notifications"}
</Button>
);
}
Timeout Control
Demonstrates the three common timeout modes: a 3-second auto-dismiss, an 8-second extended toast, and a persistent toast (timeout 0) that requires an explicit "Dismiss" action button.
"use client";
import { useRef } from "react";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export function Pattern() {
const persistentIdRef = useRef<string | null>(null);
function showPersistent() {
if (persistentIdRef.current) return;
persistentIdRef.current = toastManager.add({
actionProps: {
children: "Dismiss",
onClick: () => {
if (persistentIdRef.current) {
toastManager.close(persistentIdRef.current);
persistentIdRef.current = null;
}
},
},
description: "This toast stays until you dismiss it manually.",
timeout: 0,
title: "Persistent notification",
type: "warning",
});
}
function showTimed(ms: number) {
toastManager.add({
description: `This toast auto-dismisses after ${ms / 1000}s.`,
timeout: ms,
title: `Auto-dismiss in ${ms / 1000}s`,
type: "info",
});
}
return (
<div className="flex flex-wrap items-center gap-2">
<Button onClick={() => showTimed(3000)} variant="outline">
3s toast
</Button>
<Button onClick={() => showTimed(8000)} variant="outline">
8s toast
</Button>
<Button onClick={showPersistent} variant="outline">
Persistent toast
</Button>
</div>
);
}
Deploy Pipeline
A chained sequence of toasts that progress through a deploy flow — loading → tests passed → deployed. Each stage closes the previous toast by ID before opening the next.
"use client";
import { useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { toastManager } from "@/components/ui/toast";
type DeployState = "idle" | "running";
const steps = [
{
delay: 0,
description: "Compiling and bundling your project…",
timeout: 0,
title: "Building",
type: "loading" as const,
},
{
delay: 2500,
description: "All 48 tests passed with no failures.",
timeout: 3500,
title: "Tests passed",
type: "success" as const,
},
{
delay: 5000,
description: "Live at https://acme.vercel.app",
timeout: 6000,
title: "Deployed to production",
type: "success" as const,
},
];
export function Pattern() {
const [state, setState] = useState<DeployState>("idle");
const activeIdRef = useRef<string | null>(null);
function deploy() {
if (state === "running") return;
setState("running");
steps.forEach(({ delay, timeout, ...toast }, i) => {
setTimeout(() => {
if (activeIdRef.current) {
toastManager.close(activeIdRef.current);
activeIdRef.current = null;
}
activeIdRef.current = toastManager.add({ ...toast, timeout });
if (i === steps.length - 1) {
setTimeout(() => setState("idle"), 1000);
}
}, delay);
});
}
return (
<Button disabled={state === "running"} onClick={deploy} variant="outline">
{state === "running" ? (
<>
<Spinner />
Deploying…
</>
) : (
"Deploy to production"
)}
</Button>
);
}
Anchored Copy Toast
Uses anchoredToastManager with tooltipStyle: true to pop a "Copied!" tooltip directly above a copy button — no full notification stack needed.
npx shadcn@latest add @cnippet/ui"use client";
import { CopyIcon } from "lucide-react";
import { useRef } from "react";
import { Button } from "@/components/ui/button";
import { anchoredToastManager } from "@/components/ui/toast";
const SNIPPET = "npx shadcn@latest add @cnippet/ui";
export function Pattern() {
const btnRef = useRef<HTMLButtonElement>(null);
const copy = () => {
void navigator.clipboard.writeText(SNIPPET);
anchoredToastManager.add({
data: { tooltipStyle: true },
positionerProps: { anchor: btnRef.current },
title: "Copied!",
});
};
return (
<div className="flex items-center gap-0 overflow-hidden rounded-lg border border-input bg-muted/40 py-1 pr-1 pl-3">
<code className="flex-1 font-mono text-foreground/80 text-sm">
{SNIPPET}
</code>
<Button
className="ml-2 shrink-0"
onClick={copy}
ref={btnRef}
size="sm"
variant="ghost"
>
<CopyIcon className="size-3.5" />
Copy
</Button>
</div>
);
}
Invite with Undo
Each team invite fires a success toast with an Undo action. Clicking Undo closes the toast, removes the invite, and fires an info toast confirming the revocation.
Invite team members
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
const members = [
{
email: "olivia@example.com",
id: "olivia",
initials: "OM",
name: "Olivia Martin",
role: "Editor",
},
{
email: "jackson@example.com",
id: "jackson",
initials: "JL",
name: "Jackson Lee",
role: "Viewer",
},
{
email: "isabella@example.com",
id: "isabella",
initials: "IN",
name: "Isabella Nguyen",
role: "Editor",
},
];
export function Pattern() {
const [invited, setInvited] = useState<Set<string>>(new Set());
const invite = (member: (typeof members)[0]) => {
setInvited((prev) => new Set(prev).add(member.id));
const id = toastManager.add({
actionProps: {
children: "Undo",
onClick: () => {
toastManager.close(id);
setInvited((prev) => {
const next = new Set(prev);
next.delete(member.id);
return next;
});
toastManager.add({
description: `${member.name}'s invite has been revoked.`,
title: "Invite revoked",
type: "info",
});
},
},
description: `${member.email} will receive an email invite.`,
timeout: 6000,
title: `Invite sent to ${member.name}`,
type: "success",
});
};
return (
<div className="w-full max-w-xs rounded-xl border border-border p-4">
<p className="mb-3 font-semibold text-sm">Invite team members</p>
<div className="flex flex-col gap-2">
{members.map((m) => (
<div className="flex items-center justify-between gap-2" key={m.id}>
<div className="flex items-center gap-2.5">
<div className="flex size-7 items-center justify-center rounded-full bg-muted font-medium text-xs">
{m.initials}
</div>
<div className="flex flex-col">
<span className="font-medium text-sm leading-none">
{m.name}
</span>
<span className="text-muted-foreground text-xs">{m.role}</span>
</div>
</div>
<Button
disabled={invited.has(m.id)}
onClick={() => invite(m)}
size="sm"
variant="outline"
>
{invited.has(m.id) ? "Invited" : "Invite"}
</Button>
</div>
))}
</div>
</div>
);
}
Background Export
An Export to CSV button triggers a loading toast while work runs in the background. When complete, it closes the loading toast and opens a success toast with a Download action.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
type ExportState = "idle" | "running";
export function Pattern() {
const [state, setState] = useState<ExportState>("idle");
const runExport = () => {
if (state === "running") return;
setState("running");
const id = toastManager.add({
description: "Preparing your CSV file…",
timeout: 0,
title: "Exporting data",
type: "loading",
});
setTimeout(() => {
toastManager.close(id);
toastManager.add({
actionProps: {
children: "Download",
onClick: () => {
toastManager.add({
description: "report-2025-q1.csv saved to your downloads.",
title: "File saved",
type: "success",
});
},
},
description: "1,842 rows · report-2025-q1.csv",
timeout: 8000,
title: "Export ready",
type: "success",
});
setState("idle");
}, 2800);
};
return (
<Button
disabled={state === "running"}
onClick={runExport}
variant="outline"
>
{state === "running" ? "Exporting…" : "Export to CSV"}
</Button>
);
}
Session Expiry Warning
Simulates a session timeout: a persistent warning toast appears with a live countdown and a "Renew session" action. If ignored the timer reaches zero and an error toast replaces it.
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
const SESSION_SECONDS = 10;
export function Pattern() {
const [active, setActive] = useState(false);
const [remaining, setRemaining] = useState(SESSION_SECONDS);
const toastIdRef = useRef<string | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const clearSession = useCallback(() => {
if (intervalRef.current) clearInterval(intervalRef.current);
if (toastIdRef.current) toastManager.close(toastIdRef.current);
toastIdRef.current = null;
intervalRef.current = null;
setActive(false);
setRemaining(SESSION_SECONDS);
}, []);
const renewSession = () => {
clearSession();
toastManager.add({
description: "You're good to go for another 30 minutes.",
title: "Session renewed",
type: "success",
});
};
const startWarning = () => {
if (active) return;
setActive(true);
setRemaining(SESSION_SECONDS);
const id = toastManager.add({
actionProps: {
children: "Renew session",
onClick: renewSession,
},
description: `Session expires in ${SESSION_SECONDS}s. Renew to stay logged in.`,
timeout: 0,
title: "Session expiring soon",
type: "warning",
});
toastIdRef.current = id;
let secs = SESSION_SECONDS;
intervalRef.current = setInterval(() => {
secs -= 1;
setRemaining(secs);
if (secs <= 0) {
clearSession();
toastManager.add({
description: "Please sign in again to continue.",
title: "Session expired",
type: "error",
});
}
}, 1000);
};
useEffect(() => () => clearSession(), [clearSession]);
return (
<div className="flex flex-col items-center gap-3">
<Button disabled={active} onClick={startWarning} variant="outline">
{active
? `Session expires in ${remaining}s…`
: "Simulate session warning"}
</Button>
{active && (
<Button onClick={renewSession} size="sm" variant="ghost">
Renew session now
</Button>
)}
</div>
);
}
Unsaved Changes
A persistent "Unsaved changes" warning toast appears when the document is edited. The inline "Save now" action and the explicit Save button both resolve the toast and confirm with a success notification.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
type SaveState = "clean" | "dirty" | "saving";
export function Pattern() {
const [saveState, setSaveState] = useState<SaveState>("clean");
const [content, setContent] = useState(
"Project Alpha\n\nThis is the project description. Click below to make changes and see the unsaved changes toast appear.",
);
const toastIdRef = { current: null as string | null };
const makeChange = () => {
if (saveState === "dirty") return;
setContent((c) => `${c} Updated.`);
setSaveState("dirty");
const id = toastManager.add({
actionProps: {
children: "Save now",
onClick: () => save(id),
},
description: "Your recent edits haven't been saved yet.",
timeout: 0,
title: "Unsaved changes",
type: "warning",
});
toastIdRef.current = id;
};
const save = (id?: string) => {
const toastId = id ?? toastIdRef.current;
if (toastId) toastManager.close(toastId);
setSaveState("saving");
setTimeout(() => {
setSaveState("clean");
toastManager.add({
description: "All changes have been saved to the server.",
title: "Document saved",
type: "success",
});
}, 900);
};
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<div className="min-h-24 rounded-lg border border-input bg-background p-3 text-foreground/80 text-sm leading-relaxed">
{content}
</div>
<div className="flex gap-2">
<Button
className="flex-1"
disabled={saveState !== "clean"}
onClick={makeChange}
variant="outline"
>
Edit document
</Button>
<Button
className="flex-1"
disabled={saveState !== "dirty"}
loading={saveState === "saving"}
onClick={() => save()}
>
Save
</Button>
</div>
</div>
);
}

