Brief, non-blocking notifications that stack in a corner of the screen, powered by sonner. Typed toasts with themed icons, descriptions, actions, promise and loading states, in-place updates, custom content and six positions.
Mount <Toaster /> once, near the root of your app:
tsx
// app/layout.tsximport { Toaster } from "@/components/ui/toast";export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Toaster /> </body> </html> );}
Then call toast() from anywhere — event handlers, effects, even outside React:
tsx
import { toast } from "@/components/ui/toast";toast.success("Changes saved");
One Toaster per app
Toasts only appear where a <Toaster /> is mounted, and every mounted
<Toaster /> without an id renders every toast, so two of them show
duplicates. Mount it once in the root layout, not inside pages or dialogs.
Toast — short feedback about something that just happened ("Saved", "Invite sent", "Deploy failed"), or background work the user doesn't have to wait on.
Alert — a message that belongs to the page and must stay visible until the situation changes, such as a form error or a billing problem.
Dialog — when the user must respond before continuing.
Don't put the only way to do something in a toast; it disappears. Actions like "Undo" should be shortcuts for something also reachable elsewhere.
<Toaster /> {/* <section aria-live="polite"> › <ol> per position */}toast(title, { icon, // leading icon (typed toasts get one by default) description, // secondary line action, // primary button cancel, // secondary button});
Part
Renders
Notes
Toaster
<section> › <ol>
The live region and one list per position in use. Themed with popover tokens.
Toast
<li data-sonner-toast>
One notification. Carries data-type (success, error, …).
Icon
<div data-icon>
Lucide icon per type; tinted with the matching status color.
Title
<div data-title>
The first argument to toast().
Description
<div data-description>
Muted secondary text.
Action / Cancel
<button>
From action and cancel. The action closes the toast unless its onClick calls event.preventDefault(); cancel always closes it.
Close button
<button aria-label="Close toast">
Shown when closeButton is set on the toast or the Toaster. Never shown on loading or custom toasts.
toast() shows a plain message. toast.success, toast.info, toast.warning and toast.error add a tinted icon that matches the status colors of the rest of the library.
import { Button } from "@/components/ui/button";import { toast } from "@/components/ui/toast";export default function ToastTypes() { return ( <div className="flex flex-wrap justify-center gap-2"> <Button variant="outline" onPress={() => toast("Draft saved")}> Default </Button> <Button variant="outline" onPress={() => toast.success("Changes saved")}> Success </Button> <Button variant="outline" onPress={() => toast.info("A new version is available")} > Info </Button> <Button variant="outline" onPress={() => toast.warning("You're using 92% of your storage")} > Warning </Button> <Button variant="outline" onPress={() => toast.error("Payment failed")}> Error </Button> </div> );}
action renders a primary button, cancel a secondary one. Both take a label and an onClick, and close the toast after running. Use them for quick follow-ups like "Undo" or "View".
toast.promise shows a loading toast, then replaces it with the result. success and error can be functions that receive the resolved value or the rejection reason.
Every call returns an id. Pass it back as id to update the same toast in place — for example, to walk a toast.loading toast through several steps before it succeeds.
import { Button } from "@/components/ui/button";import { toast } from "@/components/ui/toast";export default function ToastUpdate() { async function sync() { const id = toast.loading("Syncing contacts…"); await new Promise((r) => setTimeout(r, 1200)); toast.loading("Merging duplicates…", { id }); await new Promise((r) => setTimeout(r, 1200)); toast.success("Contacts synced", { id, description: "342 contacts imported, 12 duplicates merged.", }); } return ( <Button variant="outline" onPress={sync}> Sync contacts </Button> );}
Toasts close after 4 seconds by default. Shorten duration for trivial confirmations, lengthen it for anything with text to read, and use Infinity with closeButton for ongoing states. Timers pause while the user hovers the stack or the tab is hidden.
import { Button } from "@/components/ui/button";import { toast } from "@/components/ui/toast";export default function ToastDuration() { return ( <div className="flex flex-wrap justify-center gap-2"> <Button variant="outline" onPress={() => toast("Link copied", { duration: 1500 })} > Short (1.5s) </Button> <Button variant="outline" onPress={() => toast.warning("Your session expires in 5 minutes", { duration: 10000, closeButton: true, }) } > Long with close button </Button> <Button variant="outline" onPress={() => toast.error("Connection lost", { description: "Reconnecting… Changes are saved locally.", duration: Number.POSITIVE_INFINITY, closeButton: true, }) } > Until dismissed </Button> </div> );}
richColors: true tints the whole toast with sonner's success, info, warning and error palettes. Set it on the <Toaster /> to apply it to every toast. The palettes switch to their dark variants automatically when your app is in dark mode (see Color mode); toggle the docs theme to compare.
toast.custom renders your own JSX and keeps sonner's stacking, timing and swipe-to-dismiss. The render function receives the toast's id so buttons inside can dismiss it.
import { Avatar } from "@/components/ui/avatar";import { Button } from "@/components/ui/button";import { toast } from "@/components/ui/toast";export default function ToastCustom() { return ( <Button variant="outline" onPress={() => toast.custom( (id) => ( <div className="flex w-(--width) gap-3 rounded-lg bg-popover p-4 text-popover-foreground text-sm"> <Avatar alt="Maya Chen" fallback="MC" colorful /> <div className="flex min-w-0 flex-1 flex-col gap-2"> <p> <span className="font-medium">Maya Chen</span> mentioned you in <span className="font-medium">Q3 roadmap</span> </p> <p className="line-clamp-2 text-muted-foreground"> “Can you take a look at the pricing section before Friday?” </p> <div className="flex gap-2"> <Button size="xs" onPress={() => toast.dismiss(id)}> Reply </Button> <Button size="xs" variant="ghost" onPress={() => toast.dismiss(id)} > Dismiss </Button> </div> </div> </div> ), { duration: 8000 }, ) } > Show notification </Button> );}
Submit a form, keep the button pending until the promise settles, and let toast.promise report success or the server's error message. finally resets the button either way.
import { useState } from "react";import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { TextField } from "@/components/ui/text-field";import { toast } from "@/components/ui/toast";function saveProfile(data: FormData) { return new Promise<string>((resolve, reject) => setTimeout(() => { const handle = String(data.get("handle")); if (handle === "admin") reject(new Error("That handle is reserved.")); else resolve(handle); }, 1200), );}export default function ToastRecipeSaveForm() { const [pending, setPending] = useState(false); return ( <Form className="grid w-full max-w-sm gap-4 rounded-xl border bg-card p-5" onSubmit={async (e) => { e.preventDefault(); setPending(true); const save = saveProfile(new FormData(e.currentTarget)); toast.promise(save, { loading: "Saving profile…", success: (handle) => ({ message: "Profile updated", description: `Your public URL is acme.com/@${handle}`, }), error: (err: Error) => ({ message: "Couldn't save profile", description: err.message, }), finally: () => setPending(false), }); }} > <TextField label="Display name" name="name" defaultValue="Maya Chen" /> <TextField label="Handle" name="handle" defaultValue="maya" description="Try “admin” to see the error state." isRequired /> <Button type="submit" isPending={pending} className="justify-self-end"> Save </Button> </Form> );}
The Toaster renders a <section> with aria-live="polite", so new toasts are announced without interrupting and without moving focus.
The region is labelled "Notifications" plus the hotkey. Press Alt+T to expand the stack and move focus into it; each toast is focusable.
Timers pause while the stack is expanded (hovered, or opened with the hotkey), while a toast is being pressed or swiped, and while the page is hidden.
sonner disables its own transitions and animations under prefers-reduced-motion, and the Lucide loading icon stops spinning (motion-reduce:animate-none).
Toasts disappear. Keep messages short, give important ones a longer duration, and never make a toast the only way to reach an action.
For errors that block the user, prefer an Alert near the cause, or pair the toast with one.
Toaster maps sonner's colors to your tokens: --normal-bg is --popover, --normal-text is --popover-foreground, --normal-border is --border and --border-radius is --radius. It also sets toastOptions.classNames for the toast shadow, muted descriptions, primary action buttons, muted cancel buttons and per-type icon colors, and replaces the icons with Lucide ones.
Your props are merged with these defaults instead of replacing them:
Prop
How it merges
className
Joined with the built-in toaster group classes (tailwind-merge).
style
Shallow-merged over the token variables, so you can override one, such as --width, and keep the rest.
icons
Merged per type. Passing only success keeps the Lucide info, warning, error and loading icons.
toastOptions
Every option is passed through; toastOptions.classNames is merged key by key with cn, so your toast or actionButton classes are added to the defaults.
sonner's own styles aren't in a cascade layer, so the defaults use the ! (important) modifier. Use ! on your classes too when they need to beat a sonner style or a default, for example !bg-card.
sonner's richColors and invert palettes depend on its theme. By default Toaster follows the .dark class on <html>, the same switch the theme tokens use, and watches it with a MutationObserver, so toasts change palette the moment your theme toggle runs. This works with next-themes (attribute="class"), fumadocs and hand-rolled toggles alike, and adds no dependency. During server rendering and hydration it uses "light", then switches after hydration.
Pass theme to take over: "light" or "dark" to pin it, or "system" to follow prefers-color-scheme instead of the class (useful if your app themes with media queries rather than .dark).
toast.custom content still receives the toast classes (border, radius and shadow) on its <li>, but no background, padding or width. Give your root element bg-popover, padding and w-(--width) (356px by default).