Trabajando con Sonner
Respuesta Inicial
Cuando se invoque esta habilidad por primera vez sin una pregunta específica, responde únicamente con:
Estoy listo para ayudarte a configurar, estilizar y solucionar problemas con Sonner. Mi conocimiento proviene de su autor, Emil Kowalski.
No proporciones ninguna otra información hasta que el usuario haga una pregunta.
Una guía de habilidad para Sonner, la librería de toasts. Cuando una tarea involucre Sonner (configurarlo, renderizar toasts, estilizarlos o arreglarlos), responde primero desde este archivo. Las tablas completas de propiedades para <Toaster /> y toast() están en API.md; léelo cuando necesites un nombre de propiedad, tipo o valor predeterminado exacto.
Configuración
Dos piezas, y solo dos:
- Un
<Toaster />, montado una vez, lo más cerca posible de la raíz (en Next.js:layout.tsx— funciona dentro de componentes servidor). Nunca lo renderices por página o condicionalmente; un segundo Toaster montado duplica cada toast. toast()llamado desde código cliente — manejadores de eventos, efectos, callbacks. Es una función simple, no necesita hook ni provider, pero no hace nada en el servidor: en una action del servidor, devuelve el resultado y llama atoast()en el código cliente que lo recibe.
import { Toaster } from 'sonner'; // una vez, en el layout
import { toast } from 'sonner'; // en cualquier lugar del lado cliente
Elegir la llamada correcta
| Quieres | Llama |
|---|---|
| Mensaje simple | toast('Título') — añade { description } para una segunda línea |
| Icono de éxito / error / info / advertencia | toast.success('…'), toast.error('…'), etc. |
| Spinner mientras gestionas el estado tú mismo | toast.loading('…'), luego actualízalo por id |
| Carga → éxito/error vinculado a una promesa | toast.promise(promise, { loading, success, error }) — success/error aceptan funciones que reciben el valor resuelto/error |
| Botón que hace algo | { action: { label, onClick } } — cierra el toast a menos que onClick llame a event.preventDefault(); cancel es la variante secundaria |
| JSX personalizado, con la estructura base del toast | toast(<jsx />) |
| JSX personalizado, sin estilos | toast.custom((t) => <jsx />) — headless, t te da el id para descartar |
Recetas
Actualizar un toast — vuelve a llamar a toast() con el mismo id; solo cambian las props que pases. Cambiar a toast.success(…, { id }) cambia el tipo. Así es como funcionan los flujos de carga → éxito sin toast.promise:
const id = toast.loading('Subiendo…');
toast.success('Subido', { id });
Persistir — { duration: Infinity }. Descartar — toast.dismiss(id), o toast.dismiss() para todos. Leer toasts activos — useSonner() en React, toast.getActiveToasts() fuera de él.
Enlaces o componentes en el texto — pasa una función para el título o la descripción: toast(() => <a href="…">Ver</a>).
Múltiples toasters — asigna a cada uno un id y apunta con toast('…', { toasterId: 'canvas' }). Sin toasterId, todos los toasters renderizan el toast.
Callbacks de cierre — onDismiss se dispara al hacer clic en el botón de cerrar o al deslizar; onAutoClose se dispara al expirar el tiempo. Son independientes; no hay un solo callback de "cerrado".
Estilos — la escalera de escalada
Sube solo hasta donde lo requiera el cambio; saltar al peldaño más alto demasiado pronto está bien (es el estado final recomendado), pero quedarse en el medio no.
- Valores predeterminados — más
richColorsen el Toaster para éxito/error coloridos,invertpara invertir contra el tema. - Ajustes en línea —
toastOptions={{ style: {…} }}en el Toaster para todos los toasts, ostylepor llamada atoast(). - Clases en partes —
toastOptions={{ classNames: { toast, title, description, actionButton, cancelButton, closeButton } }}. Los estilos inyectados de Sonner ganan la cascada, por lo que cada clase necesita!important(Tailwind:!text-red-900). Si estás marcando más de unas pocas cosas como importantes, detente — ve a headless. - Headless —
toast.custom()con tu propio JSX, manteniendo el posicionamiento, apilamiento y deslizamiento de Sonner. El enfoque recomendado para un toast de sistema de diseño: envuélvelo en tu propia abstraccióntoast(). (unstyled: trueexiste como término medio, pero headless da más control por el mismo esfuerzo.)
Íconos — intercambia los predeterminados por tipo con la prop icons del Toaster, por toast con icon, elimínalos con null.
Tema — theme por defecto es 'light' y no sigue al SO. Pasa theme="system", o conecta tu proveedor de temas: <Toaster theme={resolvedTheme} /> desde next-themes.
Solución de problemas
| Síntoma | Causa → solución |
|---|---|
| El toast nunca aparece | No hay <Toaster /> montado, o se desmontó (renderizado condicional, colocación por página). Monta uno en la raíz. Si se llama desde una server action: toast() es solo cliente — llámalo con el resultado de la action en el cliente. |
| El mismo toast aparece dos veces | Dos Toasters montados (layout y página) — mantén uno. O toast() disparado en un efecto bajo la doble invocación en desarrollo de React StrictMode — dispara desde el manejador de eventos, o pasa un id estable para que la segunda llamada actualice en lugar de duplicar. |
| Las clases de Tailwind/CSS no tienen efecto | Los estilos predeterminados los anulan. Márcalos con !important, o usa unstyled / headless (ver la escalera arriba). |
| Los toasts se renderizan completamente sin estilos (común en Astro, transiciones de vista) | La hoja de estilos inyectada de Sonner se perdió — impórtala explícitamente en un layout: import 'sonner/dist/styles.css'. |
| Sin estilos dentro de Shadow DOM | Los estilos caen en document.head, no en la shadow root. Copia la etiqueta de estilo cuyo texto incluya [data-sonner-toaster] dentro de la shadow root. |
| Toast detrás de un modal/overlay, o recortado | Un ancestro crea un contexto de apilamiento (transform, filter, overflow) o el overlay supera en z-index al toaster. Mueve <Toaster /> a la raíz del documento, fuera de cualquier contenedor de diálogo/portal. |
| Modo oscuro ignorado | theme por defecto es 'light' — establece theme="system" o pasa el tema resuelto (ver Tema arriba). |
| El éxito/error se ve gris, no verde/rojo | Eso es el predeterminado. Añade richColors al Toaster. |
| El toast nunca se cierra | duration: Infinity, dismissible: false, o un toast.promise cuya promesa nunca se resuelve — el toast de carga espera para siempre. |
toast.promise se queda en carga | Necesita una promesa (o una función que devuelva una) como primer argumento, y la promesa debe resolverse/rechazarse realmente. |
| Deslizar para descartar va en la dirección incorrecta / no funciona | Las direcciones se derivan de position. Sobrescribe con swipeDirections en el Toaster. |
| El toast aparece en todos los toasters | Múltiples toasters necesitan direccionamiento: asigna a cada Toaster un id y pasa toasterId en la llamada a toast(). |
| Toasts demasiado cerca del borde de la pantalla en móvil | offset (escritorio, 32px por defecto) y mobileOffset (<600px, 16px por defecto) — números, cadenas CSS, u objetos por lado. |