Trabalhando com Sonner
Resposta Inicial
Quando esta skill for invocada pela primeira vez sem uma pergunta específica, responda apenas com:
Estou pronto para ajudá-lo a configurar, estilizar e solucionar problemas com Sonner. Meu conhecimento vem de seu autor, Emil Kowalski.
Não forneça nenhuma outra informação até que o usuário faça uma pergunta.
Uma skill guia para Sonner, a biblioteca de toasts. Quando uma tarefa envolve Sonner — configurá-la, renderizar toasts, estilizá-los ou corrigi-los — responda a partir deste arquivo primeiro. Tabelas completas de props para <Toaster /> e toast() estão em API.md; leia-o quando precisar de um nome de prop exato, tipo ou valor padrão.
Configuração
Duas peças, e apenas duas:
- Um
<Toaster />, montado uma vez, o mais próximo possível da raiz (em Next.js:layout.tsx— funciona dentro de componentes de servidor). Nunca o renderize por página ou condicionalmente; um segundo Toaster montado duplica cada toast. toast()chamado a partir do código cliente — manipuladores de eventos, efeitos, callbacks. É uma função simples, sem necessidade de hook ou provider, mas não faz nada no servidor: em uma server action, retorne o resultado e chametoast()no código cliente que o recebe.
import { Toaster } from 'sonner'; // uma vez, no layout
import { toast } from 'sonner'; // em qualquer lugar no lado do cliente
Escolhendo a chamada correta
| Você quer | Chamada |
|---|---|
| Mensagem simples | toast('Título') — adicione { description } para uma segunda linha |
| Ícone de sucesso / erro / info / aviso | toast.success('…'), toast.error('…'), etc. |
| Spinner enquanto você gerencia o estado manualmente | toast.loading('…'), então atualize-o pelo id |
| Carregamento → sucesso/erro vinculado a uma promise | toast.promise(promise, { loading, success, error }) — success/error aceitam funções que recebem o valor resolvido/erro |
| Botão que faz algo | { action: { label, onClick } } — fecha o toast a menos que onClick chame event.preventDefault(); cancel é a variante secundária |
| JSX personalizado, shell de toast padrão | toast(<jsx />) |
| JSX personalizado, sem estilos | toast.custom((t) => <jsx />) — headless, t fornece o id para descartar |
Receitas
Atualizar um toast — chame toast() novamente com o mesmo id; apenas as props que você passar mudam. Mudar para toast.success(…, { id }) altera o tipo. É assim que fluxos de loading → success funcionam sem toast.promise:
const id = toast.loading('Enviando…');
toast.success('Enviado', { id });
Persistir — { duration: Infinity }. Dispensar — toast.dismiss(id), ou toast.dismiss() para todos. Ler toasts ativos — useSonner() no React, toast.getActiveToasts() fora dele.
Links ou componentes no texto — passe uma função para o título ou descrição: toast(() => <a href="…">Ver</a>).
Múltiplos toasters — dê a cada um um id e direcione com toast('…', { toasterId: 'canvas' }). Sem toasterId, cada toaster renderiza o toast.
Callbacks de fechamento — onDismiss dispara no botão de fechar ou deslize; onAutoClose dispara no timeout. Eles são separados; não existe um único callback "fechado".
Estilização — a escada de escalada
Suba apenas até onde a mudança exigir; pular para o degrau mais alto muito cedo é aceitável (é o estado final recomendado), mas permanecer no meio não é.
- Padrões — mais
richColorsno Toaster para sucesso/erro coloridos,invertpara inverter contra o tema. - Ajustes inline —
toastOptions={{ style: {…} }}no Toaster para todos os toasts, oustylepor chamadatoast(). - Classes nas partes —
toastOptions={{ classNames: { toast, title, description, actionButton, cancelButton, closeButton } }}. Os estilos injetados do Sonner vencem a cascata, então toda classe precisa de!important(Tailwind:!text-red-900). Se você estiver marcando mais do que algumas coisas como important, pare — vá para headless. - Headless —
toast.custom()com seu próprio JSX, mantendo o posicionamento, empilhamento e deslize do Sonner. A abordagem recomendada para um toast de design system: envolva-o em sua própria abstraçãotoast(). (unstyled: trueexiste como um meio-termo, mas headless dá mais controle pelo mesmo esforço.)
Ícones — troque os padrões por tipo com a prop icons do Toaster, por toast com icon, remova com null.
Tema — theme padrão é 'light' e não acompanha o SO. Passe theme="system", ou conecte seu provedor de tema: <Toaster theme={resolvedTheme} /> de next-themes.
Solução de Problemas
| Sintoma | Causa → Correção |
|---|---|
| Toast nunca aparece | Nenhum <Toaster /> montado, ou ele foi desmontado (renderização condicional, posicionamento por página). Monte um na raiz. Se chamado de uma server action: toast() é apenas cliente — chame-o com o resultado da ação no cliente. |
| Mesmo toast aparece duas vezes | Dois Toasters montados (layout e página) — mantenha um. Ou toast() disparado em um efeito sob a dupla invocação de desenvolvimento do React StrictMode — dispare a partir do manipulador de evento, ou passe um id estável para que a segunda chamada atualize em vez de duplicar. |
| Classes CSS/Tailwind não têm efeito | Os estilos padrão os sobrescrevem. Marque-os com !important, ou use unstyled / headless (veja a escada acima). |
| Toasts renderizam completamente sem estilo (comum em Astro, transições de visualização) | A folha de estilo injetada do Sonner foi perdida — importe-a explicitamente em um layout: import 'sonner/dist/styles.css'. |
| Sem estilo dentro de Shadow DOM | Os estilos vão para document.head, não para a shadow root. Copie a tag de estilo cujo texto inclui [data-sonner-toaster] para a shadow root. |
| Toast atrás de um modal/overlay, ou cortado | Um ancestral cria um contexto de empilhamento (transform, filter, overflow) ou o overlay tem um z-index maior que o toaster. Mova <Toaster /> para a raiz do documento, fora de qualquer contêiner de diálogo/portal. |
| Modo escuro ignorado | theme padrão é 'light' — defina theme="system" ou passe o tema resolvido (veja Tema acima). |
| Sucesso/erro parecem cinza, não verde/vermelho | Esse é o padrão. Adicione richColors ao Toaster. |
| Toast nunca fecha | duration: Infinity, dismissible: false, ou uma toast.promise cuja promise nunca se resolve — o toast de loading espera para sempre. |
toast.promise preso em loading | Precisa de uma promise (ou uma função que retorna uma) como primeiro argumento, e a promise deve de fato resolver/rejeitar. |
| Deslizar para descartar vai na direção errada / não funciona | As direções derivam de position. Substitua com swipeDirections no Toaster. |
| Toast aparece em todos os toasters | Múltiplos toasters precisam de direcionamento: dê a cada Toaster um id e passe toasterId na chamada toast(). |
| Toasts muito próximos da borda da tela no mobile | offset (desktop, padrão 32px) e mobileOffset (<600px, padrão 16px) — números, strings CSS, ou objetos por lado. |