Cómo crear notificaciones toast en Svelte
Crea notificaciones toast en Svelte con un store writable o usa svelte-sonner, con sintaxis Svelte 5, accesibilidad y auto-cierre.
Una notificación toast en Svelte es un mensaje pequeño y transitorio que aparece sobre tu interfaz para confirmar una acción o informar de un error, y que se cierra solo tras un tiempo de espera.
La mayoría acabamos escribiendo una de madrugada, justo después de que el envío de un formulario funcione y la página se quede ahí, como si no hubiera pasado nada. Tienes dos caminos sólidos: construir tú mismo un sistema ligero con un store writable y un componente contenedor, o incorporar una librería mantenida. Este artículo muestra ambos con código listo para copiar y pegar, cubre la accesibilidad y señala dónde los tutoriales antiguos de Svelte 4 usan sintaxis que ahora está obsoleta en Svelte 5.
Todo lo que sigue apunta a Svelte 5, la versión mayor estable actual, estable desde octubre de 2024. Cuando Svelte 4 difiere, la diferencia se indica sobre la marcha.
Puntos clave
- En Svelte 5 el store de toasts apenas cambia (
writable([])sigue funcionando), pero el componente de toast debe migrarse:export letpasa a ser$props,on:clickpasa a seronclick,<slot />se convierte en un snippet ycreateEventDispatcherse reemplaza por una prop de callback. - Para que los lectores de pantalla anuncien los toasts, renderízalos dentro de un contenedor con
aria-live(politepara info/éxito,assertivepara errores).role="alert"en cada toast es una alternativa a ese contenedor, no un añadido: combinar ambos puede provocar que un mismo mensaje se anuncie dos veces. - Asigna a cada toast un id a prueba de colisiones con
crypto.randomUUID()y limpia su temporizador de autocierre cuando se elimine manualmente, para que un toast descartado a mano nunca dispare una eliminación obsoleta. - svelte-sonner se instala con
npm i svelte-sonner, se renderiza una sola vez como<Toaster />en la raíz de la aplicación y luego se dispara desde cualquier lugar mediantetoast(),toast.success(),toast.error()otoast.promise(). - Hazlo tú mismo cuando quieras cero dependencias y control total; recurre a svelte-sonner cuando quieras toasts basados en promesas, descarte por deslizamiento, temas y accesibilidad resueltos de fábrica.
¿Qué es un toast y cuándo deberías usarlo?
Un toast es feedback transitorio y no bloqueante: mensajes de éxito/error/información que se apilan, se cierran solos con un temporizador y nunca interrumpen al usuario como lo hace un modal. Recurre a un toast para confirmar el envío de un formulario, mostrar un error asíncrono o dar acuse de recibo de una acción en segundo plano. No lo uses para contenido sobre el que el usuario deba actuar o que no pueda perderse. Eso corresponde a un mensaje en línea o a un diálogo, porque un toast puede cerrarse solo antes de que se lea.
¿Cómo se construye un sistema de toasts en Svelte con un store?
Discover how at OpenReplay.com.
El núcleo de un sistema hecho a mano es un único store writable que contiene un array de objetos toast, más los helpers addToast/dismissToast que puedes llamar desde cualquier sitio. Los stores de Svelte siguen funcionando en Svelte 5, así que este patrón no está obsoleto. El enfoque más reciente con runas en .svelte.ts es idiomático, pero opcional.
// src/lib/toast-store.js
import { writable } from 'svelte/store';
export const toasts = writable([]);
const timers = new Map();
export function addToast(toast) {
const id = crypto.randomUUID();
const defaults = { id, type: 'info', dismissible: true, timeout: 3000 };
const t = { ...defaults, ...toast };
toasts.update((all) => [t, ...all]);
if (t.timeout) {
timers.set(id, setTimeout(() => dismissToast(id), t.timeout));
}
return id;
}
export function dismissToast(id) {
const timer = timers.get(id);
if (timer) {
clearTimeout(timer); // stop a stale auto-dismiss from firing later
timers.delete(id);
}
toasts.update((all) => all.filter((t) => t.id !== id));
}
Aquí conviene afinar dos detalles de corrección. Los IDs provienen de crypto.randomUUID() en lugar de Math.random(), de modo que no pueden colisionar (solo se ejecuta en un contexto seguro, es decir, HTTPS o localhost). Y el temporizador de cada toast se registra en un Map y se limpia al descartarlo manualmente, así que pulsar el botón de cerrar nunca deja un setTimeout apuntando a un toast ya eliminado.
Ahora el contenedor renderiza el array, con clave por id, y entrega a cada toast un callback de descarte:
<!-- src/lib/Toasts.svelte -->
<script>
import Toast from './Toast.svelte';
import { toasts, dismissToast } from './toast-store.js';
</script>
<section class="toast-container" role="region" aria-live="polite" aria-label="Notifications">
{#each $toasts as toast (toast.id)}
<Toast {...toast} ondismiss={() => dismissToast(toast.id)} />
{/each}
</section>
<style>
.toast-container {
position: fixed; top: 1rem; left: 0; right: 0;
display: flex; flex-direction: column; align-items: center;
gap: 0.5rem; z-index: 1000; pointer-events: none;
}
</style>
El componente hijo Toast.svelte usa expresiones idiomáticas de Svelte 5 de principio a fin: $props() para las entradas, onclick para el evento y una prop de callback para el descarte:
<!-- src/lib/Toast.svelte (Svelte 5) -->
<script>
import { fade } from 'svelte/transition';
let { message, type = 'info', dismissible = true, ondismiss } = $props();
</script>
<article class="toast {type}" transition:fade>
<p>{message}</p>
{#if dismissible}
<button class="close" onclick={() => ondismiss?.()} aria-label="Dismiss notification">×</button>
{/if}
</article>
<style>
.toast { display: flex; gap: 1rem; width: 20rem; padding: 0.75rem 1.25rem;
border-radius: 0.25rem; color: white; pointer-events: auto; }
.info { background: SteelBlue; }
.success { background: SeaGreen; }
.error { background: IndianRed; }
.close { margin-left: auto; background: none; border: 0; color: inherit;
font-size: 1.25rem; cursor: pointer; }
</style>
Monta <Toasts /> una sola vez en tu layout raíz y luego dispáralo desde donde quieras:
import { addToast } from '$lib/toast-store.js';
addToast({ message: 'Saved!', type: 'success' });
Svelte 4 frente a Svelte 5: la sintaxis que cambió
Si estás copiando un tutorial antiguo de dev.to, el store es portable, pero el componente no. En Svelte 5, export let se reemplaza por $props, on:click pasa a ser el atributo onclick y <slot /> se sustituye por snippets. Y lo más importante: createEventDispatcher está obsoleto; un botón de descarte debería llamar a una prop de callback (ondismiss?.()) en lugar de emitir un evento. La versión de Toast.svelte en Svelte 4 empezaría con export let type = 'info', import { createEventDispatcher } y usaría on:click={() => dispatch('dismiss')}, todos ellos patrones obsoletos en un proyecto Svelte 5.
Variantes, posicionamiento y accesibilidad
Tres detalles de UX separan un toast que funciona de uno bueno: variantes, transiciones y compatibilidad con lectores de pantalla. Las variantes no son más que un campo type mapeado a colores de fondo (info/success/error), la transición fade de svelte/transition anima la entrada y la salida, y un contenedor con position: fixed y un z-index alto mantiene los toasts fijados por encima de la página.
La accesibilidad merece su propio apartado. Tienes dos formas de conseguir que se anuncie un toast, y deberías elegir exactamente una. role="alert" en cada toast implica aria-live="assertive", y los navegadores sí dan un trato especial a los nodos de tipo alert: MDN señala que su contenido se anuncia en la mayoría de los casos, incluso cuando el nodo se inyecta en la página después de la carga. El problema es que esto varía según la combinación de navegador y lector de pantalla, así que una región activa (live region) persistente que ya esté en el DOM es la opción más predecible, y por eso el contenedor del código anterior lleva aria-live="polite" y el toast en sí no lleva ningún rol. Usa polite para información y éxito, de modo que los anuncios se encolen detrás de lo que el usuario esté haciendo, y cambia un contenedor (o una segunda región) a assertive para los errores que requieran atención inmediata.
Combinar ambos es el error que hay que evitar. MDN advierte de que juntar aria-live y role="alert" provoca doble locución en VoiceOver en iOS, y una alerta assertive renderizada dentro de una región polite invita al mismo anuncio duplicado. Las repeticiones de sesión de implementaciones de toasts revelan con frecuencia el fallo en el que un toast se disparó y se cerró solo, pero nunca fue percibido: la ausencia de una región activa significaba que no se anunciaba nada.
Usa una librería en su lugar: svelte-sonner
svelte-sonner es la vía lista para usar, y está construida para Svelte 5. Es un port a Svelte de Sonner, de Emil Kowalski, y conserva los mismos valores por defecto con criterio. Instala el paquete, monta un único <Toaster /> cerca de la raíz de tu aplicación y cada toast que dispares desde cualquier otro punto del código se renderizará dentro de él.
<script>
import { Toaster, toast } from 'svelte-sonner';
</script>
<Toaster richColors closeButton position="top-center" duration={5000} />
<button onclick={() => toast.success('Event has been created')}>Success</button>
<button onclick={() => toast.error('Event has not been created')}>Error</button>
La recompensa por asumir la dependencia es toast.promise(), que se abre en estado de carga y luego se sustituye a sí mismo por un mensaje de éxito o error una vez que la promesa se resuelve. Ese es el único patrón que resulta realmente tedioso de implementar a mano:
toast.promise(saveEvent(), {
loading: 'Saving…',
success: (data) => `${data.name} saved!`,
error: 'Could not save'
});
<Toaster /> acepta las props position, richColors, closeButton y duration, y con Tailwind puedes dar estilo a los toasts tú mismo pasando un objeto toastOptions que contenga unstyled: true junto con un mapa classes. El descarte por deslizamiento y el foco por teclado (⌥/alt + T) vienen incorporados. npm i svelte-sonner resuelve a una build 1.x; la entrada más reciente en las notas de versión del proyecto es la v1.1.1, que corrigió un fallo por el cual los toasts configurados para no expirar nunca se descartaban en el momento en que se actualizaban.
Dos alternativas. svelte-french-toast merece la pena conocerla, pero su versión estable publicada es de la era de Svelte 4, así que los usuarios de Svelte 5 necesitan un fork como svelte-hot-french-toast. La otra es @zerodevx/svelte-toast, cuya línea v0 actual declara peer dependencies que abarcan Svelte 3, 4 y 5.
Hacerlo tú mismo frente a svelte-sonner: cómo elegir
Hazlo tú mismo cuando quieras cero dependencias, control total sobre el marcado o aprender los stores de Svelte; recurre a svelte-sonner cuando quieras toasts basados en promesas, descarte por deslizamiento, temas y accesibilidad resueltos de fábrica.
| Necesidad | Hacerlo tú mismo | svelte-sonner |
|---|---|---|
| Dependencias | Ninguna | Un paquete |
| Control del marcado | Total | Mediante toastOptions (unstyled + classes) |
| Toasts con promesas | A mano | toast.promise() incorporado |
| Descarte por deslizamiento | Por tu cuenta | Incorporado |
| Accesibilidad | Tú cableas aria-live | Resuelta |
| Listo para Svelte 5 | Sí (con runas/props de callback) | Sí, de forma nativa |
Entre las librerías, svelte-sonner apunta directamente a Svelte 5; la svelte-french-toast original es de la era de Svelte 4, y la línea v0 de @zerodevx/svelte-toast funciona en Svelte 3, 4 y 5.
Empieza con la versión basada en store si tus necesidades se limitan a éxito/error/información con autocierre. Son unas 60 líneas y te enseña el patrón de store. En cuanto necesites feedback basado en promesas o gestos de deslizamiento, instala svelte-sonner y borra tu código personalizado. Elijas lo que elijas, configura primero la región aria-live; es el detalle que resulta fácil de omitir y difícil de echar en falta.
Preguntas frecuentes
¿Sigue funcionando createEventDispatcher en Svelte 5?
Sigue ejecutándose, pero está obsoleto en Svelte 5, de modo que los componentes existentes que lo usan continúan funcionando, mientras que el código nuevo no debería adoptarlo. El reemplazo oficial para emitir eventos como el descarte de un toast es una prop de callback, por ejemplo pasar una función ondismiss y llamar a ondismiss?.() desde el botón de cerrar. La documentación de Svelte enumera las props de callback y la runa $host() como las alternativas recomendadas.
¿Debería cada toast usar role='alert', o debería el contenedor usar aria-live?
Cualquiera de los dos enfoques puede funcionar, pero usa uno y no ambos. Los navegadores dan un tratamiento especial a role='alert' y en la mayoría de los casos anuncian su contenido incluso cuando el nodo se inserta después de la carga de la página, aunque esto varía según la combinación de navegador y lector de pantalla. Un contenedor persistente que ya existe en el DOM y lleva aria-live es la opción más predecible: aria-live='polite' para información y éxito, y 'assertive' para errores. Hacer ambas cosas a la vez arriesga un anuncio duplicado, y MDN señala que combinar aria-live y role='alert' provoca doble locución en VoiceOver en iOS.
¿Cuál es la diferencia entre svelte-sonner y svelte-french-toast para Svelte 5?
svelte-sonner apunta directamente a Svelte 5 y se instala como una build 1.x con toasts basados en promesas, descarte por deslizamiento, richColors y botón de cerrar. La versión estable publicada de svelte-french-toast es de la era de Svelte 4 y su última versión estable es anterior a Svelte 5, así que los usuarios de Svelte 5 necesitan un fork como svelte-hot-french-toast. Existe una svelte-french-toast 2.0.0-alpha, pero no se ha publicado como versión estable en npm.
¿Puedo seguir usando un store writable para los toasts en Svelte 5, o debo cambiar a runas?
Un store writable sigue funcionando en Svelte 5 y no está obsoleto, por lo que un store de toasts construido con writable([]) más los helpers de añadir y descartar es completamente válido. Las runas en un archivo .svelte.ts son el patrón idiomático más reciente para el estado reactivo compartido, pero son opcionales. Lo que debe migrarse a la sintaxis de Svelte 5 es el componente que consume el store, no el store en sí.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k