Toast
BetaConfirmación breve y transitoria de algo que la persona acaba de hacer.
registry/ds/toast.tsx"use client";
import { Button } from "@/registry/ds/button";
import { ToastProvider, useToast } from "@/registry/ds/toast";
function Triggers() {
const toast = useToast();
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button variant="tonal" size="sm" onClick={() => toast({ color: "success", title: "Cambios guardados" })}>
Éxito
</Button>
<Button
variant="tonal"
size="sm"
onClick={() =>
toast({
color: "danger",
title: "No se pudo enviar",
description: "Revisá tu conexión. Tu borrador quedó guardado.",
action: { label: "Reintentar", onClick: () => {} },
})
}
>
Error con acción
</Button>
<Button variant="tonal" size="sm" onClick={() => toast({ title: "Enlace copiado" })}>
Neutro
</Button>
</div>
);
}
// En una app real, <ToastProvider> va una sola vez en el layout raíz
export default function ToastBasic() {
return (
<ToastProvider>
<Triggers />
</ToastProvider>
);
}Cuándo usarlo
Un toast confirma: "guardado", "copiado", "enviado". Desaparece solo, así que nunca puede llevar información que la persona necesite después.
| Usá Toast para… | Usá otra cosa para… |
|---|---|
| Confirmar una acción que ya terminó. | Errores que hay que leer y resolver → Alert. |
| Ofrecer deshacer ("Mensaje archivado · Deshacer"). | Errores de un campo → error del Input. |
| Avisos de fondo que no bloquean. | Decisiones ("¿Seguro?") → Modal. |
Siguiendo una promesa
toast.promise() muestra "cargando" (con spinner, sin cierre automático) y
se convierte en éxito o error con lo que devuelva la promesa. Un solo toast,
no tres.
"use client";
import { useState } from "react";
import { Button } from "@/registry/ds/button";
import { Switch } from "@/registry/ds/switch";
import { ToastProvider, useToast } from "@/registry/ds/toast";
function Form() {
const toast = useToast();
const [falla, setFalla] = useState(false);
const guardar = () =>
new Promise<string>((resolve, reject) => {
window.setTimeout(() => (falla ? reject(new Error("Sin conexión")) : resolve("3 cambios")), 1500);
});
return (
<div className="flex flex-col items-center gap-4">
<Button
variant="tonal"
onClick={() =>
toast.promise(guardar(), {
loading: "Guardando cambios…",
success: (data) => ({ title: "Cambios guardados", description: `Se actualizaron ${data}.` }),
error: { title: "No se pudo guardar", description: "Revisá la conexión. Tu borrador sigue acá.", action: { label: "Reintentar", onClick: () => {} } },
})
}
>
Guardar cambios
</Button>
<Switch size="sm" label="Simular error" checked={falla} onCheckedChange={setFalla} />
</div>
);
}
/** Un toast que sigue una promesa: muestra "cargando", y se convierte en éxito o error con lo que devuelva */
export default function ToastPromise() {
return (
<ToastProvider>
<Form />
</ToastProvider>
);
}Apilado, persistente y prioridad
Se apilan de a tres y los demás esperan su turno. timeout: 0 deja el toast
hasta que lo cierren: para acciones con Deshacer. Los danger interrumpen
al lector de pantalla; el resto, no.
"use client";
import { Button } from "@/registry/ds/button";
import { ToastProvider, useToast } from "@/registry/ds/toast";
function Triggers() {
const toast = useToast();
const materias = ["Cálculo II", "Física I", "Álgebra"];
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button variant="tonal" size="sm" onClick={() => materias.forEach((m, i) => window.setTimeout(() => toast({ color: "success", title: `Inscripción a ${m} confirmada` }), i * 250))}>
Tres seguidos
</Button>
<Button
variant="tonal"
size="sm"
onClick={() =>
toast({
color: "warning",
title: "Materia dada de baja",
description: "Liberaste tu lugar en Física I.",
timeout: 0,
action: { label: "Deshacer", onClick: () => {} },
})
}
>
Con Deshacer (no se cierra solo)
</Button>
<Button variant="tonal" size="sm" onClick={() => toast({ color: "danger", title: "Se cortó la sesión", description: "Volvé a ingresar para seguir." })}>
Error (prioridad alta)
</Button>
</div>
);
}
/** Se apilan de a 3 (los demás esperan), se expanden al pasar el mouse, y se pueden arrastrar para cerrar. El error interrumpe al lector de pantalla */
export default function ToastStack() {
return (
<ToastProvider limit={3}>
<Triggers />
</ToastProvider>
);
}Reglas
- Una línea de título; descripción solo si suma.
- Máximo una acción, y que no sea la única forma de hacerla.
- 5 segundos por defecto. Con acción o texto largo, dale más (
timeout). - Se apilan hasta 3; los más viejos se ocultan. Si estás disparando muchos, agrupá ("3 archivos subidos").
- Un error puede ir en toast solo si es recuperable y no pierde datos. Si no, Alert.
Medidas
| Parte | Medida |
|---|---|
| Card | 380 de ancho (100% − 32 en mobile) · padding spacing-2xl 16 · radius-lg 10 |
| Posición | Abajo a la derecha, a 24 del borde (16 en mobile) |
| Icono | 20 · gap 12 con el texto |
| Título / descripción | Text sm / Medium · Text sm |
| Apilado | 12 de separación; los de atrás asoman 12 y escalan −10% |
Tokens
| Parte | Propiedad | Token |
|---|---|---|
| Card | Fondo | bg/elevated |
| Card | Borde | border/default |
| Card | Sombra | Elevation/lg |
| Icono | Color, por rol | success/accent |
| Título | Color | text/default |
| Descripción | Color | text/muted |
| Acción | Color | primary/accent |
| Cerrar | Icono | icon/subtle |
El toast es siempre una card neutra: el color vive solo en el icono. Así no compite con la página ni depende del color para entenderse.
En Figma
| Propiedad en Figma | Valores | En código | Nota |
|---|---|---|---|
| Color | Neutral · Info · Success · Warning · Error | color | — |
| Title · Description | Texto | title · description | — |
| Action label · Show action | Texto · on/off | action | — |
| Dismissible | on/off | — | En código siempre se puede cerrar. |
Instalación
npx shadcn@latest add https://polen.cdc.cool/r/toast.jsonConfiguración
El provider va una vez, en el layout raíz:
import { ToastProvider } from "@/components/ds/toast";
export default function Layout({ children }) {
return <ToastProvider>{children}</ToastProvider>;
}Y desde cualquier componente cliente:
const toast = useToast();
toast({ color: "success", title: "Cambios guardados" });Playground
Configurá el componente y copiá el snippet: se genera del mismo estado que renderiza el preview.
const toast = useToast();
toast({
color: "success",
title: "Cambios guardados",
});Opciones de toast()
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
title | string | — | Requerido. Qué pasó. |
description | string | — | Detalle opcional. |
color | "neutral" | "info" | "success" | "warning" | "danger" | "neutral" | Define el icono. |
timeout | number | 5000 | ms hasta cerrarse. 0 = no se cierra solo. |
action | { label, onClick } | — | Una acción, tipo Deshacer o Reintentar. |
ToastProvider acepta timeout (default global) y limit (cuántos se ven a
la vez, 3). Sobre Base UI Toast.
toast.promise()
Para una operación asíncrona, un solo toast que cambia de estado:
toast.promise(guardar(), {
loading: "Guardando cambios…",
success: (data) => ({ title: "Cambios guardados", description: `Se actualizaron ${data}.` }),
error: { title: "No se pudo guardar", action: { label: "Reintentar", onClick: retry } },
});Cada estado acepta un string (el título), las mismas opciones de toast(), o
una función que las arma con el resultado o el error. Devuelve la promesa
original, así podés encadenar .then(); si rechaza, no hace falta un catch
solo para el toast.
Qué viene resuelto
- Se anuncia a lectores de pantalla sin mover el foco. Los
dangerse anuncian con prioridad alta; el resto espera su turno. - Se pausa al pasar el mouse o al enfocarlo: nadie pierde un mensaje por leer despacio.
- F6 lleva el foco a la pila de toasts; Esc la cierra.
- Se descarta deslizando hacia abajo o a la derecha.
- Sin animación de entrada con
prefers-reduced-motion.
Qué tenés que cuidar vos
- Nunca pongas en un toast lo único que prueba que algo pasó. El estado tiene que verse también en la página.
- Si lleva acción, subí el
timeout: 5 segundos no alcanzan para leer y decidir.