React
Toasts with Sonner: choices and traps
Mount one Toaster in the layout and call toast() from events, never from render. Sonner announces the message; duration and context stay your choice.
Key points
- One `
` per application: two instances announce the same message twice. - Call `toast()` from an event handler or an effect, never during render.
- The default duration is 4 seconds: important messages need `closeButton` or more time.
Short answer: install the component with npx shadcn@latest add sonner, mount <Toaster /> once in the layout, and call toast() from an event handler. Sonner owns the queue, the animation and the live region; position, duration and tone remain yours.
Mount it once
The Toaster is the container that draws the messages. It belongs in the root layout, after the children, not in individual pages. Two instances in the same tree create two live regions, so a screen reader announces the same text twice.
// app/layout.tsx
import { Toaster } from "@/components/ui/sonner";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
The file is a client component, yet the layout can stay a server component: importing it is enough. position accepts top-left, top-center, top-right, bottom-left, bottom-center and bottom-right; richColors tints the background by outcome; closeButton adds the X.
The three calls that cover almost everything
The bare call carries information, toast.success and toast.error carry an outcome, toast.promise handles an outcome that arrives later. The promise case saves the most code: Sonner updates one toast through its three states instead of stacking three.
"use client";
import { toast } from "sonner";
export function SaveButton() {
async function onSubmit() {
await toast.promise(saveDraft(), {
loading: "Salvataggio in corso…",
success: "Bozza salvata",
error: (error) => `Salvataggio fallito: ${error.message}`,
});
}
return Salva;
}
Traps that show up in production
| Trap | What happens | Fix |
|---|---|---|
| <Toaster /> on every page | Each mount adds a live region and the message is announced more than once. | One instance in the root layout. |
| toast() in the component body | A toast fires on every render, including StrictMode renders in development. | Call it inside a handler or inside an effect with explicit dependencies. |
| toast.error(error) | The object is coerced to a string and the message becomes unreadable. | Pass text: toast.error(error.message). |
| A very long duration | The toast covers the controls in the bottom-right corner and never expires. | 4-6 seconds, or closeButton plus manual dismissal. |
| Theme not passed to the Toaster | Toasts stay light on a dark interface. | Read the theme from the provider and pass it to theme, or use toastOptions. |
Accessibility: time is part of the message
Sonner writes every toast into a live region: the text is announced and focus stays where it was. Duration is where trouble starts. A message that disappears after 4 seconds fails WCAG 2.2.1 when it holds the only available action, because the user cannot reach it in time. For errors and destructive actions, use closeButton and a longer duration, or keep the message until it is dismissed.
A named limitation
Sonner draws toasts over the document and takes no part in layout: an end-to-end test that looks for the message inside the page container never finds it. The catalogue, measured on 1 November 2026, holds no toast blocks to copy, because a block is a static composition and a toast exists only at runtime. The component is documented in the sonner record, not in a block.
Where to continue
In the catalogue you find the shadcn library, the sonner component record and a second toast record. For installation read what shadcn/ui is and how to install shadcn/ui; to see what adding blocks to the catalogue costs, 1,556 blocks between preview and Pro.
Frequently asked questions
Where does the Toaster go?
Once, in the root layout, after the children. Two instances in the same tree announce the same message twice and confuse screen reader users.
Why does the toast appear twice?
Almost always two Toaster instances, or a call inside an effect without a guard: in development StrictMode runs the effect twice. Check the layout before changing the logic.
Can I call toast() from a Server Component?
No. toast() writes to the DOM and Sonner is a client component: the call belongs in a file with "use client" or inside a handler, not in the body of a server component.
How do I show the result of a promise?
Use toast.promise(promise, { loading, success, error }). Sonner reuses one toast for the three states and passes the error to the error function, which must return a string.
How long should a toast stay?
Four seconds work for information. When the message holds an action or an error, add closeButton and extend the duration, otherwise you fail WCAG 2.2.1 on timing.