Toast
Notificaciones toast con Sonner — toast de éxito, error, carga y personalizados en shadcn/ui.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
npx shadcn@latest add sonner// app/layout.tsx — añade el Toaster
import { Toaster } from "@/components/ui/sonner";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
);
}// Uso en cualquier lugar
import { toast } from "sonner";
toast("Notificación predeterminada");
toast.success("¡Guardado correctamente!");
toast.error("Algo salió mal");
toast.info("¿Sabías que...?");
toast.warning("Revisa tu entrada");
toast.loading("Procesando...");Cuándo usarlo: Cuando necesites retroalimentación no bloqueante para acciones del usuario — guardados, eliminaciones, errores y progreso de tareas en segundo plano.
Ejemplo funcional
"use client";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
export function ToastShowcase() {
// Toasts simples
function showSuccess() {
toast.success("Perfil actualizado", {
description: "Tus cambios se han guardado.",
});
}
function showError() {
toast.error("Error al guardar", {
description: "Comprueba tu conexión e inténtalo de nuevo.",
action: {
label: "Reintentar",
onClick: () => console.log("Retrying..."),
},
});
}
// Toast con promise — muestra carga, luego éxito o error
function handleSave() {
const savePromise = new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.3 ? resolve("done") : reject(new Error("fail"));
}, 2000);
});
toast.promise(savePromise, {
loading: "Guardando cambios...",
success: "¡Cambios guardados!",
error: "Error al guardar. Inténtalo de nuevo.",
});
}
// Toast personalizado con JSX
function showCustom() {
toast.custom((id) => (
<div className="flex items-center gap-3 rounded-lg border bg-white p-4 shadow-lg dark:bg-gray-950">
<div className="size-10 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-blue-600 text-lg">!</span>
</div>
<div className="flex-1">
<p className="text-sm font-medium">Nuevo mensaje de Alice</p>
<p className="text-xs text-muted-foreground">¡Oye, mira esta función!</p>
</div>
<Button size="sm" variant="outline" onClick={() => toast.dismiss(id)}>
Cerrar
</Button>
</div>
));
}
// Toast con deshacer
function handleDelete() {
let undone = false;
toast("Elemento eliminado", {
description: "El elemento se ha movido a la papelera.",
action: {
label: "Deshacer",
onClick: () => {
undone = true;
toast.success("¡Restaurado!");
},
},
onAutoClose: () => {
if (!undone) {
// Eliminar realmente
console.log("Permanently deleted");
}
},
});
}
return (
<div className="flex flex-wrap gap-3">
<Button onClick={showSuccess}>Éxito</Button>
<Button variant="destructive" onClick={showError}>Error + Reintentar</Button>
<Button variant="outline" onClick={handleSave}>Toast con promise</Button>
<Button variant="secondary" onClick={showCustom}>Personalizado</Button>
<Button variant="ghost" onClick={handleDelete}>Eliminar con deshacer</Button>
</div>
);
}Lo que esto demuestra:
- Variantes de toast de éxito, error e info
- Texto de descripción para contexto adicional
- Botón de acción (Reintentar, Deshacer) dentro del toast
toast.promisepara operaciones async con estados de carga/éxito/errortoast.custompara toasts JSX totalmente personalizados- Patrón de deshacer con ejecución retardada
Profundización
Cómo funciona
- Sonner es una librería de toast de Emil Kowalski — shadcn la envuelve como el componente
<Toaster> <Toaster>renderiza un contenedor de posición fija que gestiona el apilamiento y las animaciones de los toaststoast()es una llamada a función (no un hook) — invócala desde cualquier lugar, incluidos callbacks de server actions- Los toasts se cierran automáticamente tras una duración configurable (predeterminado ~4 segundos)
- Varios toasts se apilan verticalmente con animaciones suaves
- Los toasts admiten deslizar para cerrar en móvil
Variaciones
Configuración del Toaster:
<Toaster
position="top-right" // o "top-left", "bottom-left", "bottom-right", "top-center", "bottom-center"
richColors // habilita fondos de color para éxito/error
closeButton // muestra un botón de cierre en cada toast
duration={5000} // duración predeterminada de cierre automático en ms
expand={false} // si los toasts se expanden para mostrarse todos a la vez
toastOptions={{
className: "border-border",
descriptionClassName: "text-muted-foreground",
}}
/>Toast en respuesta de server action:
// Server action
"use server";
export async function updateProfile(formData: FormData) {
// ... lógica de actualización
return { success: true, message: "Perfil actualizado" };
}
// Componente cliente
async function handleSubmit(formData: FormData) {
const result = await updateProfile(formData);
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
}Toast persistente (sin cierre automático):
const toastId = toast.loading("Subiendo archivo...", {
duration: Infinity,
});
// Más tarde, actualízalo
toast.success("¡Carga completada!", { id: toastId });
// O ciérralo
toast.dismiss(toastId);Toasts temáticos con richColors:
// Con <Toaster richColors />:
toast.success("¡Guardado!"); // fondo verde
toast.error("¡Error!"); // fondo rojo
toast.info("Nota:"); // fondo azul
toast.warning("Precaución:"); // fondo amarilloNotas de TypeScript
// toast devuelve un ID de tipo string
const id: string | number = toast("Hola");
// Cerrar un toast específico
toast.dismiss(id);
// Cerrar todos los toasts
toast.dismiss();
// toast.promise es genérico
const promise: Promise<User> = fetchUser();
toast.promise(promise, {
loading: "Cargando usuario...",
success: (user) => `¡Bienvenido, ${user.name}!`, // user está tipado como User
error: (err) => `Error: ${err.message}`,
});
// Función de renderizado de toast personalizado
toast.custom((id: string | number) => (
<div>Contenido personalizado <button onClick={() => toast.dismiss(id)}>Cerrar</button></div>
));Errores comunes
-
Falta
<Toaster />— Si olvidas añadir<Toaster />a tu layout, las llamadas atoast()no hacen nada. Solución: Añádelo a tu layout raíz. -
Toast invocado durante el renderizado — Llamar a
toast()dentro del cuerpo de un componente (no en un manejador de eventos) se dispara durante el renderizado. Solución: Llama atoast()enuseEffect, manejadores de eventos o después de server actions. -
Demasiados toasts — Acciones rápidas pueden inundar la pantalla. Solución: Usa
toast.dismiss()antes de mostrar un toast nuevo, o usaidpara actualizar un toast existente. -
richColorsy modo oscuro — SinrichColors, los toasts de éxito/error se ven igual. Con él, obtienen fondos de color que pueden necesitar ajustes en modo oscuro. Solución: Prueba ambos temas. -
Sonner vs toast de shadcn — shadcn tenía anteriormente su propio componente toast. La recomendación actual es Sonner. Solución: Usa
npx shadcn@latest add sonner, no el antiguo componentetoast.
Alternativas
| Alternativa | Úsalo cuando | No lo uses cuando |
|---|---|---|
| react-hot-toast | Quieres una alternativa ligera con API similar | Sonner cubre tus necesidades |
| Notificaciones nativas del navegador | Necesitas notificaciones a nivel de SO (con permiso) | La retroalimentación en la app es suficiente |
| Alertas inline | La retroalimentación está ligada a una sección específica de la página | Necesitas retroalimentación no bloqueante y transitoria |
| Snackbar (MUI) | Usas Material UI | Usas shadcn/ui |
FAQs
¿Cuál es el primer paso para habilitar toasts en un proyecto shadcn?
- Añade
<Toaster />a tu layout raíz (app/layout.tsx) - Sin él, las llamadas a
toast()no hacen nada en silencio - Instala con
npx shadcn@latest add sonner
¿Qué variantes de toast están disponibles de serie?
toast()— notificación predeterminadatoast.success()— retroalimentación de éxitotoast.error()— retroalimentación de errortoast.info()— informativotoast.warning()— advertenciatoast.loading()— spinner de carga
¿Cómo funciona toast.promise para operaciones async?
toast.promise(savePromise, {
loading: "Guardando cambios...",
success: "¡Cambios guardados!",
error: "Error al guardar. Inténtalo de nuevo.",
});Transiciona automáticamente entre estados de carga, éxito y error según la promise.
¿Cómo implementas un patrón de deshacer con toast?
- Muestra un toast con un botón de acción "Deshacer"
- Rastrea si se hizo clic en deshacer con una variable local
- Usa
onAutoClosepara ejecutar la eliminación real solo si no se hizo clic en deshacer
Gotcha: ¿Qué ocurre si llamas a toast() dentro del cuerpo de un componente durante el renderizado?
- Se dispara durante la fase de renderizado, lo que puede causar comportamiento inesperado
- Solución: llama a
toast()solo en manejadores de eventos,useEffecto después de respuestas de server actions
¿Cómo creas un toast persistente que no se cierre automáticamente?
const toastId = toast.loading("Subiendo archivo...", {
duration: Infinity,
});
// Más tarde, actualízalo o ciérralo:
toast.success("¡Carga completada!", { id: toastId });¿Cómo actualizas un toast existente en lugar de crear uno nuevo?
- Guarda el ID de toast devuelto por
toast()otoast.loading() - Pasa
{ id: toastId }a una llamada posterior atoast.success()otoast.error() - Esto reemplaza el contenido del toast existente in situ
¿Qué hace la prop richColors en Toaster?
- Habilita fondos de color para toasts tipados (verde para éxito, rojo para error, etc.)
- Sin ella, todos los tipos de toast se ven igual
- Prueba en modo claro y oscuro, ya que los colores pueden necesitar ajustes
¿Cómo renderizas JSX totalmente personalizado en un toast?
toast.custom((id) => (
<div className="flex items-center gap-3 rounded-lg border p-4 shadow-lg">
<p className="text-sm font-medium">Mensaje personalizado</p>
<Button size="sm" onClick={() => toast.dismiss(id)}>Cerrar</Button>
</div>
));Gotcha: ¿Cuál es la diferencia entre el antiguo toast de shadcn y el toast actual basado en Sonner?
- shadcn incluía anteriormente su propio componente toast con una API diferente
- La recomendación actual es Sonner (
npx shadcn@latest add sonner) - No uses el antiguo componente
toasten proyectos nuevos
¿Cómo se tipa toast.promise en TypeScript?
const promise: Promise<User> = fetchUser();
toast.promise(promise, {
loading: "Cargando usuario...",
success: (user) => `¡Bienvenido, ${user.name}!`, // user está tipado como User
error: (err) => `Error: ${err.message}`,
});¿Qué posiciones están disponibles para el componente Toaster?
top-right,top-left,top-centerbottom-right,bottom-left,bottom-center- Configúralo con
<Toaster position="top-right" />