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

TrapWhat happensFix
<Toaster /> on every pageEach mount adds a live region and the message is announced more than once.One instance in the root layout.
toast() in the component bodyA 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 durationThe 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 ToasterToasts 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.