Toast
Toast notifications with Sonner — success, error, loading, and custom toasts in shadcn/ui.
Recipe
Quick-reference recipe card — copy-paste ready.
npx shadcn@latest add sonner// app/layout.tsx — add the Toaster
import { Toaster } from "@/components/ui/sonner";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
);
}// Usage anywhere
import { toast } from "sonner";
toast("Default notification");
toast.success("Saved successfully!");
toast.error("Something went wrong");
toast.info("Did you know?");
toast.warning("Check your input");
toast.loading("Processing...");When to reach for this: When you need non-blocking feedback for user actions — saves, deletes, errors, and background task progress.
Working Example
"use client";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
export function ToastShowcase() {
// Simple toasts
function showSuccess() {
toast.success("Profile updated", {
description: "Your changes have been saved.",
});
}
function showError() {
toast.error("Failed to save", {
description: "Please check your connection and try again.",
action: {
label: "Retry",
onClick: () => console.log("Retrying..."),
},
});
}
// Promise toast — shows loading, then success or error
function handleSave() {
const savePromise = new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.3 ? resolve("done") : reject(new Error("fail"));
}, 2000);
});
toast.promise(savePromise, {
loading: "Saving changes...",
success: "Changes saved!",
error: "Failed to save. Try again.",
});
}
// Custom toast with JSX
function showCustom() {
toast.custom((id) => (
<div className="flex items-center gap-3 rounded-lg border bg-white p-4 shadow-lg dark:bg-gray-950">
<div className="size-10 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-blue-600 text-lg">!</span>
</div>
<div className="flex-1">
<p className="text-sm font-medium">New message from Alice</p>
<p className="text-xs text-muted-foreground">Hey, check out this feature!</p>
</div>
<Button size="sm" variant="outline" onClick={() => toast.dismiss(id)}>
Dismiss
</Button>
</div>
));
}
// Undo toast
function handleDelete() {
let undone = false;
toast("Item deleted", {
description: "The item has been moved to trash.",
action: {
label: "Undo",
onClick: () => {
undone = true;
toast.success("Restored!");
},
},
onAutoClose: () => {
if (!undone) {
// Actually delete
console.log("Permanently deleted");
}
},
});
}
return (
<div className="flex flex-wrap gap-3">
<Button onClick={showSuccess}>Success</Button>
<Button variant="destructive" onClick={showError}>Error + Retry</Button>
<Button variant="outline" onClick={handleSave}>Promise Toast</Button>
<Button variant="secondary" onClick={showCustom}>Custom</Button>
<Button variant="ghost" onClick={handleDelete}>Delete with Undo</Button>
</div>
);
}What this demonstrates:
- Success, error, and info toast variants
- Description text for additional context
- Action button (Retry, Undo) inside the toast
toast.promisefor async operations with loading/success/error statestoast.customfor fully custom JSX toasts- Undo pattern with delayed execution
Deep Dive
How It Works
- Sonner is a toast library by Emil Kowalski — shadcn wraps it as the
<Toaster>component <Toaster>renders a fixed-position container that manages toast stacking and animationstoast()is a function call (not a hook) — call it from anywhere, including server action callbacks- Toasts auto-dismiss after a configurable duration (default ~4 seconds)
- Multiple toasts stack vertically with smooth animations
- Toasts support swipe-to-dismiss on mobile
Variations
Toaster configuration:
<Toaster
position="top-right" // or "top-left", "bottom-left", "bottom-right", "top-center", "bottom-center"
richColors // enables colored backgrounds for success/error
closeButton // shows a close button on each toast
duration={5000} // default auto-close duration in ms
expand={false} // whether toasts expand to show all at once
toastOptions={{
className: "border-border",
descriptionClassName: "text-muted-foreground",
}}
/>Toast in server action response:
// Server action
"use server";
export async function updateProfile(formData: FormData) {
// ... update logic
return { success: true, message: "Profile updated" };
}
// Client component
async function handleSubmit(formData: FormData) {
const result = await updateProfile(formData);
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
}Persistent toast (no auto-dismiss):
const toastId = toast.loading("Uploading file...", {
duration: Infinity,
});
// Later, update it
toast.success("Upload complete!", { id: toastId });
// Or dismiss it
toast.dismiss(toastId);Themed toasts with richColors:
// With <Toaster richColors />:
toast.success("Saved!"); // green background
toast.error("Failed!"); // red background
toast.info("Note:"); // blue background
toast.warning("Caution:"); // yellow backgroundTypeScript Notes
// toast returns a string ID
const id: string | number = toast("Hello");
// Dismiss a specific toast
toast.dismiss(id);
// Dismiss all toasts
toast.dismiss();
// toast.promise is generic
const promise: Promise<User> = fetchUser();
toast.promise(promise, {
loading: "Loading user...",
success: (user) => `Welcome, ${user.name}!`, // user is typed as User
error: (err) => `Error: ${err.message}`,
});
// Custom toast render function
toast.custom((id: string | number) => (
<div>Custom content <button onClick={() => toast.dismiss(id)}>Close</button></div>
));Gotchas
-
Missing
<Toaster />— If you forget to add<Toaster />to your layout,toast()calls do nothing. Fix: Add it to your root layout. -
Toast called during render — Calling
toast()inside a component body (not an event handler) triggers during render. Fix: Calltoast()inuseEffect, event handlers, or after server actions. -
Too many toasts — Rapid actions can flood the screen. Fix: Use
toast.dismiss()before showing a new toast, or useidto update an existing toast. -
richColorsand dark mode — WithoutrichColors, success/error toasts look the same. With it, they get colored backgrounds that may need dark mode adjustments. Fix: Test both themes. -
Sonner vs shadcn toast — shadcn previously had its own toast component. The current recommendation is Sonner. Fix: Use
npx shadcn@latest add sonner, not the oldtoastcomponent.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| react-hot-toast | You want a lightweight alternative with similar API | Sonner covers your needs |
| Native browser notifications | You need OS-level notifications (with permission) | In-app feedback is sufficient |
| Inline alerts | The feedback is tied to a specific section of the page | You need non-blocking, transient feedback |
| Snackbar (MUI) | You use Material UI | You use shadcn/ui |
FAQs
What is the first step to enable toasts in a shadcn project?
- Add
<Toaster />to your root layout (app/layout.tsx) - Without it,
toast()calls silently do nothing - Install with
npx shadcn@latest add sonner
What toast variants are available out of the box?
toast()— default notificationtoast.success()— success feedbacktoast.error()— error feedbacktoast.info()— informationaltoast.warning()— warningtoast.loading()— loading spinner
How does toast.promise work for async operations?
toast.promise(savePromise, {
loading: "Saving changes...",
success: "Changes saved!",
error: "Failed to save. Try again.",
});It automatically transitions through loading, success, and error states based on the promise.
How do you implement an undo pattern with toast?
- Show a toast with an "Undo" action button
- Track whether undo was clicked with a local variable
- Use
onAutoCloseto perform the actual deletion only if undo was not clicked
Gotcha: What happens if you call toast() inside a component body during render?
- It fires during the render phase, which can cause unexpected behavior
- Fix: only call
toast()in event handlers,useEffect, or after server action responses
How do you create a persistent toast that does not auto-dismiss?
const toastId = toast.loading("Uploading file...", {
duration: Infinity,
});
// Later, update or dismiss it:
toast.success("Upload complete!", { id: toastId });How do you update an existing toast instead of creating a new one?
- Save the toast ID returned by
toast()ortoast.loading() - Pass
{ id: toastId }to a subsequenttoast.success()ortoast.error()call - This replaces the content of the existing toast in place
What does the richColors prop on Toaster do?
- Enables colored backgrounds for typed toasts (green for success, red for error, etc.)
- Without it, all toast types look the same
- Test in both light and dark mode as colors may need adjustment
How do you render fully custom JSX in a toast?
toast.custom((id) => (
<div className="flex items-center gap-3 rounded-lg border p-4 shadow-lg">
<p className="text-sm font-medium">Custom message</p>
<Button size="sm" onClick={() => toast.dismiss(id)}>Dismiss</Button>
</div>
));Gotcha: What is the difference between the old shadcn toast and the current Sonner-based toast?
- shadcn previously shipped its own toast component with a different API
- The current recommendation is Sonner (
npx shadcn@latest add sonner) - Do not use the old
toastcomponent in new projects
How is toast.promise typed in TypeScript?
const promise: Promise<User> = fetchUser();
toast.promise(promise, {
loading: "Loading user...",
success: (user) => `Welcome, ${user.name}!`, // user is typed as User
error: (err) => `Error: ${err.message}`,
});What positions are available for the Toaster component?
top-right,top-left,top-centerbottom-right,bottom-left,bottom-center- Set via
<Toaster position="top-right" />