Toaster
Toast notifications powered by sonner. The <Toaster> component is already mounted in the root layout with design-system tokens and themed icons. Trigger toasts anywhere via the toast() function from sonner.
Anatomy
Mount <Toaster richColors />once in your root layout. The wrapper overrides Sonner's internal CSS variables with design-system tokens — normal background, text, border, and border-radius — so all toasts automatically follow the brand palette and respect dark mode.
Variants
Five semantic variants, each mapped to a custom Lucide icon registered on the Toaster instance. richColors is enabled globally so success/error/warning/info toasts use accessible color schemes.
Semantic types
Action Button
Pass an action object to add a high-emphasis inline button — useful for undo flows. Use toast.promise() for async operations that should automatically transition through loading, success, and error states.
Cancel Button
The cancel option adds a secondary dismiss button styled with a muted background. Unlike action, it signals a non-destructive exit — use it alongside action when the user needs an explicit opt-out.
Loading
Use toast.loading() to show a persistent spinner toast. Every toast() call returns an id — pass it to any subsequent toast call to update the same element in place rather than stacking a new one.
Custom Icon
Override the default icon on any individual toast via the icon option. Pass any React node — a Lucide icon, an avatar, or a status indicator. Pass null to suppress the icon entirely.
Close Button
Add a visible × button to a toast using the closeButton option. Most useful on persistent toasts (duration: Infinity) where auto-dismiss is disabled and the user must act explicitly.
Custom Toast
toast.custom() renders arbitrary JSX with no Sonner styling applied — you own every pixel. The render function receives the toast id so you can dismiss it programmatically from within the content.
Dismiss
Call toast.dismiss(id) to remove a specific toast by its returned ID. Call toast.dismiss() with no argument to clear every active toast at once.
Duration
Control how long a toast stays on screen with the duration option (milliseconds). The default is 4000ms. Pass Infinity for a toast that never auto-dismisses — combine it with closeButton or a custom dismiss action.
Duration examples
Design Guidelines
Do
- Match the variant to the semantic weight. Use
successfor completed actions,errorfor failures that need attention. - Keep descriptions short. One sentence maximum — toasts are transient and users won't read a paragraph.
- Use toast.promise() for async operations. It automatically handles all three states and avoids manual state management.
- Use cancel alongside action for reversible decisions. Give users a low-friction escape path when confirming intent.
Don't
- Don't use for critical errors. If a user needs to act on an error, use an inline Alert or Dialog — not a transient toast.
- Don't spam toasts. Multiple rapid toasts confuse users and create visual noise. Deduplicate or throttle them.
- Don't put complex content in styled toasts. Long messages, lists, or interactive forms belong in a Dialog or Sheet — or use
toast.custom().
Developer Reference
Setup & Accessibility
- Mount
<Toaster richColors />once inapp/layout.tsx. Already configured in this project. - Toasts are announced to screen readers via a live region —
aria-live="polite"for non-error toasts,aria-live="assertive"for errors. - Every
toast()call returns anid: string | number. Pass it back to anytoast()call ortoast.dismiss(id)to update or remove that specific toast.
Key Options
| Option | Type | Notes |
|---|---|---|
description | ReactNode | Secondary line below the title. |
duration | number | Ms until auto-dismiss. Default 4000. Infinity disables. |
action | { label, onClick } | High-emphasis inline button (primary style). |
cancel | { label, onClick } | Low-emphasis dismiss button (muted style). |
icon | ReactNode | null | Overrides the default icon. null hides it. |
closeButton | boolean | Shows a × button. Most useful with Infinity duration. |
id | string | number | Targets an existing toast to update it in place. |
dismissible | boolean | Whether the toast can be swiped away. Default true. |
Usage
import { toast } from "sonner"
// <Toaster richColors /> is already in app/layout.tsx
// Basic
toast("Saved successfully.")
// With description
toast("File deleted", {
description: "The file has been moved to trash.",
})
// Semantic variants
toast.success("Changes saved!")
toast.error("Failed to save.", { description: "Please try again." })
toast.warning("Unsaved changes will be lost.")
toast.info("A new version is available.")
// Action button (primary style)
toast("Item deleted", {
action: { label: "Undo", onClick: () => toast.success("Restored!") },
})
// Cancel button (muted style) alongside action
toast("Save changes?", {
action: { label: "Save", onClick: () => toast.success("Saved.") },
cancel: { label: "Discard", onClick: () => {} },
})
// Promise (loading → success/error automatically)
toast.promise(saveData(), {
loading: "Saving...",
success: "Saved!",
error: "Failed to save.",
})
// Loading → update in place via id
const id = toast.loading("Uploading...")
await upload()
toast.success("Upload complete!", { id })
// Per-toast custom icon (pass null to hide)
toast("New message", { icon: <MailIcon className="size-4 text-primary" /> })
toast("No icon", { icon: null })
// Close button (useful with persistent toasts)
toast("Update available", {
duration: Infinity,
closeButton: true,
action: { label: "Install", onClick: () => {} },
})
// Custom duration
toast("Quick note", { duration: 1000 }) // 1 second
toast("Persistent", { duration: Infinity }) // never auto-dismisses
// Custom JSX toast — full control, no Sonner styling applied
toast.custom((id) => (
<div className="rounded-lg border border-border bg-card p-4 shadow-sm">
<p className="text-sm font-medium">Custom content</p>
<button onClick={() => toast.dismiss(id)}>Dismiss</button>
</div>
))
// Dismiss
toast.dismiss(id) // remove specific toast
toast.dismiss() // remove all active toasts