# 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.

> Page: https://ui.eneawork.it/blog/shadcn-toast-sonner
> Markdown: https://ui.eneawork.it/blog/shadcn-toast-sonner.md

*2026-11-01*

Risposta breve: installa il componente con npx shadcn@latest add sonner, monta <Toaster /> una volta nel layout e chiama toast() da un gestore di eventi. Sonner gestisce coda, animazione e regione live; a te restano posizione, durata e tono del messaggio.

Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi, 12.808 icone da 1.472 librerie, letti da /api/stats il 1 novembre 2026. Dei 1.556 blocchi, nessuno contiene «toast» o «sonner» in nome, descrizione o categorie: contati da data/blocks.json lo stesso giorno. Un toast è comportamento, non layout.

## Il montaggio: una volta sola

Il Toaster è il contenitore che disegna i messaggi. Va nel layout radice, dopo i children, e non nelle singole pagine. Due istanze nello stesso albero creano due regioni live, quindi lo screen reader annuncia lo stesso testo due volte.

// app/layout.tsx import { Toaster } from "@/components/ui/sonner"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); }

Il file è un client component, ma il layout può restare un server component: basta importarlo. position accetta top-left, top-center, top-right, bottom-left, bottom-center e bottom-right; richColors colora lo sfondo per esito; closeButton aggiunge la X.

## Le tre chiamate che coprono quasi tutto

La chiamata secca serve per un'informazione, toast.success e toast.error per un esito, toast.promise quando l'esito arriva da una promise. Il caso promise è quello che evita più codice: Sonner sostituisce lo stesso toast nei tre stati invece di impilarne tre.

"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; }

## Le trappole che si vedono in produzione

## Accessibilità: il tempo è parte del messaggio

Sonner scrive ogni toast in una regione live: il testo viene annunciato e il focus resta dov'era. Il problema nasce dalla durata. Un messaggio che sparisce dopo 4 secondi non rispetta il criterio WCAG 2.2.1 se contiene l'unica azione disponibile, perché l'utente non fa in tempo a raggiungerla. Per errori e azioni distruttive usa closeButton e una durata più alta, oppure lascia il messaggio finché non viene chiuso.

## Limite dichiarato

Sonner disegna i toast sopra il documento e non partecipa al layout: un test end-to-end che cerca il messaggio dentro il contenitore della pagina non lo trova. Il catalogo, misurato il 1 novembre 2026, non ha blocchi toast da copiare, perché un blocco è una composizione statica e un toast esiste solo a runtime. Il componente è documentato nel record sonner, non in un blocco.

## Dove continuare

Nel catalogo trovi la libreria shadcn, il record del componente sonner e un secondo record toast. Per l'installazione leggi che cosa è shadcn/ui e come installare shadcn/ui; per capire quanto costa aggiungere blocchi al catalogo, 1.556 blocchi tra preview e Pro.

## FAQ

### Dove va montato il Toaster?

Una volta sola, nel layout radice, dopo i children. Due istanze nello stesso albero annunciano lo stesso messaggio due volte e confondono chi usa lo screen reader.

### Perché il toast compare due volte?

Quasi sempre per due istanze di Toaster, oppure per una chiamata dentro un effetto senza guardia: in sviluppo StrictMode esegue l'effetto due volte. Controlla il layout prima di cambiare la logica.

### Posso chiamare toast() da un Server Component?

No. toast() scrive nel DOM e Sonner è un componente client: la chiamata deve stare in un file con "use client" o dentro un handler, non nel corpo di un server component.

### Come mostro l'esito di una promise?

Usa toast.promise(promise, { loading, success, error }). Sonner riusa lo stesso toast per i tre stati e passa l'errore alla funzione error, che deve restituire una stringa.

### Quanto deve durare un toast?

Quattro secondi vanno bene per un'informazione. Se il messaggio contiene un'azione o un errore, aggiungi closeButton e allunga la durata, altrimenti violi il criterio WCAG 2.2.1 sul tempo.
