React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

shadcntoastnotificationssonner

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.promise for async operations with loading/success/error states
  • toast.custom for 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 animations
  • toast() 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 background

TypeScript 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: Call toast() in useEffect, 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 use id to update an existing toast.

  • richColors and dark mode — Without richColors, 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 old toast component.

Alternatives

AlternativeUse WhenDon't Use When
react-hot-toastYou want a lightweight alternative with similar APISonner covers your needs
Native browser notificationsYou need OS-level notifications (with permission)In-app feedback is sufficient
Inline alertsThe feedback is tied to a specific section of the pageYou need non-blocking, transient feedback
Snackbar (MUI)You use Material UIYou 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 notification
  • toast.success() — success feedback
  • toast.error() — error feedback
  • toast.info() — informational
  • toast.warning() — warning
  • toast.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 onAutoClose to 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() or toast.loading()
  • Pass { id: toastId } to a subsequent toast.success() or toast.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 toast component 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-center
  • bottom-right, bottom-left, bottom-center
  • Set via <Toaster position="top-right" />
  • Setup — adding the Toaster to your layout
  • Button — buttons that trigger toasts
  • Dialog — toast after dialog actions
  • Form — toast on form submission