Criando Notificações Toast no Svelte
Crie notificações toast em Svelte com um writable store ou use svelte-sonner, com sintaxe Svelte 5, acessibilidade e auto-fechamento.
Uma notificação toast no Svelte é uma mensagem pequena e transitória que aparece sobre sua UI para confirmar uma ação ou reportar um erro, e então se descarta após um tempo limite.
A maioria de nós acaba escrevendo uma tarde da noite, logo depois que o envio de um formulário é bem-sucedido e a página simplesmente fica ali parada, como se nada tivesse acontecido. Você tem dois caminhos sólidos: construir um sistema leve por conta própria com uma store writable mais um componente contêiner, ou incorporar uma biblioteca mantida. Este artigo mostra os dois com código pronto para copiar e colar, aborda acessibilidade e sinaliza onde tutoriais mais antigos de Svelte 4 usam sintaxe agora obsoleta no Svelte 5.
Tudo abaixo tem como alvo o Svelte 5, a versão principal estável atual, estável desde outubro de 2024. Onde o Svelte 4 difere, a diferença é anotada no texto.
Pontos Principais
- No Svelte 5, a store de toasts muda muito pouco (
writable([])continua funcionando), mas o componente de toast precisa migrar:export letpassa a ser$props,on:clickpassa a seronclick,<slot />passa a ser um snippet, ecreateEventDispatcheré substituído por uma prop de callback. - Para que leitores de tela anunciem os toasts, renderize-os dentro de um contêiner com
aria-live(politepara info/sucesso,assertivepara erros).role="alert"em cada toast é uma alternativa a esse contêiner, não um acréscimo a ele: combinar os dois pode fazer com que uma única mensagem seja anunciada duas vezes. - Dê a cada toast um id à prova de colisões com
crypto.randomUUID()e limpe seu timer de auto-dismiss quando ele for removido manualmente, para que um toast descartado à mão nunca dispare uma remoção obsoleta. - svelte-sonner é instalado com
npm i svelte-sonner, renderizado uma única vez como<Toaster />na raiz da aplicação, e então acionado de qualquer lugar viatoast(),toast.success(),toast.error()outoast.promise(). - Faça o seu próprio quando quiser zero dependências e controle total; recorra ao svelte-sonner quando quiser toasts baseados em promises, swipe-to-dismiss, temas e acessibilidade resolvidos de fábrica.
O que é um toast e quando você deve usá-lo?
Um toast é um feedback transitório e não bloqueante: mensagens de sucesso/erro/info que se empilham, se descartam automaticamente após um tempo e nunca interrompem o usuário como um modal faz. Recorra a um toast para confirmar o envio de um formulário, expor um erro assíncrono ou reconhecer uma ação em segundo plano. Não use um para conteúdo sobre o qual o usuário precisa agir ou que não pode perder. Isso pertence a uma mensagem inline ou a um diálogo, porque um toast pode se descartar automaticamente antes de ser lido.
Como construir um sistema de toasts no Svelte com uma store?
Discover how at OpenReplay.com.
O núcleo de um sistema feito à mão é uma única store writable contendo um array de objetos de toast, mais os helpers addToast/dismissToast que você pode chamar de qualquer lugar. As stores do Svelte continuam funcionando no Svelte 5, então esse padrão não está obsoleto. A abordagem mais nova com runes em .svelte.ts é idiomática, mas 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));
}
Dois detalhes de correção valem ser feitos direito aqui. Os IDs vêm de crypto.randomUUID() em vez de Math.random(), para que não possam colidir (ele só funciona em um contexto seguro, ou seja, HTTPS ou localhost). E o timer de cada toast é rastreado em um Map e limpo no descarte manual, de modo que clicar no botão de fechar nunca deixe um setTimeout apontando para um toast já removido.
Agora o contêiner renderiza o array, com chave por id, e entrega a cada toast um 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>
O componente filho Toast.svelte usa idiomas do Svelte 5 por completo: $props() para as entradas, onclick para o evento e uma prop de callback para o 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>
Monte <Toasts /> uma única vez no seu layout raiz e então acione de qualquer lugar:
import { addToast } from '$lib/toast-store.js';
addToast({ message: 'Saved!', type: 'success' });
Svelte 4 vs Svelte 5: a sintaxe que mudou
Se você está copiando um tutorial mais antigo do dev.to, a store é portável, mas o componente não é. No Svelte 5, export let é substituído por $props, on:click passa a ser o atributo onclick, e <slot /> é substituído por snippets. Mais importante ainda, createEventDispatcher está obsoleto: um botão de descarte deve chamar uma prop de callback (ondismiss?.()) em vez de despachar um evento. A versão Svelte 4 do Toast.svelte começaria com export let type = 'info', import { createEventDispatcher }, e usaria on:click={() => dispatch('dismiss')}, todos padrões obsoletos em um projeto Svelte 5.
Variantes, posicionamento e acessibilidade
Três detalhes de UX separam um toast que funciona de um toast bom: variantes, transições e suporte a leitores de tela. Variantes são apenas um campo type mapeado para cores de fundo (info/success/error), a transição fade de svelte/transition anima a entrada e a saída, e um contêiner com position: fixed e um z-index alto mantém os toasts fixados acima da página.
Acessibilidade merece uma análise própria. Você tem duas maneiras de fazer um toast ser anunciado, e deve escolher exatamente uma. role="alert" em cada toast implica aria-live="assertive", e os navegadores realmente dão tratamento especial a nós do tipo alert: a MDN observa que seu conteúdo é anunciado na maioria dos casos, inclusive quando o nó é injetado na página após o carregamento. O problema é que isso varia conforme a combinação de navegador e leitor de tela, então uma live region persistente que já esteja no DOM é a opção mais previsível, e é por isso que o contêiner no código acima carrega aria-live="polite" e o toast em si não carrega nenhum role. Use polite para info e sucesso, para que os anúncios entrem na fila atrás do que o usuário está fazendo, e mude um contêiner (ou uma segunda região) para assertive para erros que exigem atenção imediata.
Combinar os dois é o erro a evitar. A MDN alerta que colocar aria-live e role="alert" juntos causa fala duplicada no VoiceOver no iOS, e um alerta assertivo renderizado dentro de uma região polite convida ao mesmo anúncio duplicado. Session replays de implementações de toast frequentemente revelam o modo de falha em que um toast foi disparado e se descartou automaticamente, mas nunca foi percebido: nenhuma live region significava que nada foi anunciado.
Use uma biblioteca no lugar: svelte-sonner
svelte-sonner é o caminho pronto para uso, e foi construído para o Svelte 5. É um port para Svelte do Sonner de Emil Kowalski, mantendo os mesmos padrões opinativos. Instale o pacote, monte um único <Toaster /> perto da raiz da sua aplicação, e todo toast que você disparar de qualquer outro lugar da base de código será renderizado dentro dele.
<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>
A recompensa pela dependência é toast.promise(), que abre em estado de carregamento e depois se substitui por uma mensagem de sucesso ou erro quando a promise é resolvida. Esse é o único padrão que é genuinamente tedioso de implementar à mão:
toast.promise(saveEvent(), {
loading: 'Saving…',
success: (data) => `${data.name} saved!`,
error: 'Could not save'
});
<Toaster /> aceita as props position, richColors, closeButton e duration, e para Tailwind você estiliza os toasts por conta própria passando um objeto toastOptions contendo unstyled: true mais um mapa classes. Swipe-to-dismiss e foco por teclado (⌥/alt + T) já vêm embutidos. npm i svelte-sonner resolve para um build 1.x; a entrada mais recente nas release notes do projeto é a v1.1.1, que corrigiu um bug em que toasts configurados para nunca expirar eram descartados no momento em que fossem atualizados.
Duas alternativas. svelte-french-toast vale ser conhecido, mas seu release estável publicado é da era do Svelte 4, então usuários de Svelte 5 precisam de um fork como svelte-hot-french-toast. A outra é @zerodevx/svelte-toast, cuja linha v0 atual declara peer dependencies abrangendo Svelte 3, 4 e 5.
Fazer o seu próprio vs. svelte-sonner: como escolher
Faça o seu próprio quando quiser zero dependências, controle total sobre a marcação, ou aprender as stores do Svelte; recorra ao svelte-sonner quando quiser toasts baseados em promises, swipe-to-dismiss, temas e acessibilidade resolvidos de fábrica.
| Necessidade | Fazer o seu próprio | svelte-sonner |
|---|---|---|
| Dependências | Nenhuma | Um pacote |
| Controle da marcação | Total | Via toastOptions (unstyled + classes) |
| Toasts com promises | Construir à mão | toast.promise() embutido |
| Swipe-to-dismiss | Por conta própria | Embutido |
| Acessibilidade | Você mesmo configura o aria-live | Resolvido |
| Pronto para Svelte 5 | Sim (com runes/props de callback) | Sim, nativamente |
Entre as bibliotecas, o svelte-sonner tem como alvo o Svelte 5 diretamente; o svelte-french-toast original é da era do Svelte 4, e a linha v0 do @zerodevx/svelte-toast funciona com Svelte 3, 4 e 5.
Comece com a versão baseada em store se suas necessidades são sucesso/erro/info com auto-dismiss. São talvez 60 linhas e isso te ensina o padrão de stores. No momento em que você precisar de feedback orientado a promises ou gestos de swipe, instale o svelte-sonner e apague seu código customizado. Qualquer que seja a escolha, configure primeiro a região aria-live; é o único detalhe que é fácil de pular e difícil de notar quando falta.
Perguntas Frequentes
createEventDispatcher ainda funciona no Svelte 5?
Ele ainda funciona, mas está obsoleto no Svelte 5, então componentes existentes que o usam continuam funcionando, enquanto código novo não deveria adotá-lo. A substituição oficial para emitir eventos como o descarte de um toast é uma prop de callback, como passar uma função ondismiss e chamar ondismiss?.() no botão de fechar. A documentação do Svelte lista props de callback e a rune $host() como as alternativas recomendadas.
Cada toast deve usar role='alert' ou o contêiner deve usar aria-live?
Qualquer uma das abordagens pode funcionar, mas use uma e não as duas. Os navegadores dão tratamento especial a role='alert' e na maioria dos casos anunciam seu conteúdo mesmo quando o nó é inserido após o carregamento da página, embora isso varie conforme as combinações de navegador e leitor de tela. Um contêiner persistente que já exista no DOM e carregue aria-live é a opção mais previsível: aria-live='polite' para info e sucesso, 'assertive' para erros. Fazer as duas coisas ao mesmo tempo corre o risco de um anúncio duplicado, e a MDN observa que combinar aria-live e role='alert' causa fala duplicada no VoiceOver no iOS.
Qual é a diferença entre svelte-sonner e svelte-french-toast para o Svelte 5?
O svelte-sonner tem como alvo o Svelte 5 diretamente e é instalado como um build 1.x com toasts baseados em promises, swipe-to-dismiss, richColors e um botão de fechar. O svelte-french-toast estável publicado é da era do Svelte 4 e seu último release estável antecede o Svelte 5, então usuários de Svelte 5 precisam de um fork como svelte-hot-french-toast. Existe um svelte-french-toast 2.0.0-alpha, mas ele não foi publicado como release estável no npm.
Posso continuar usando uma store writable para toasts no Svelte 5, ou preciso migrar para runes?
Uma store writable continua funcionando no Svelte 5 e não está obsoleta, então uma store toasts construída com writable([]) mais helpers de adicionar e descartar é totalmente válida. Runes em um arquivo .svelte.ts são o padrão idiomático mais novo para estado reativo compartilhado, mas são opcionais. O que precisa migrar para a sintaxe do Svelte 5 é o componente que consome a store, não a store em si.
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