12k
All articles

Создание toast-уведомлений в Svelte

Создавайте toast-уведомления в Svelte через writable store или svelte-sonner, с синтаксисом Svelte 5, доступностью и автозакрытием.

OpenReplay Team
OpenReplay Team
Создание toast-уведомлений в Svelte

Toast-уведомление в Svelte — это небольшое временное сообщение, которое появляется поверх интерфейса, чтобы подтвердить действие или сообщить об ошибке, а затем само исчезает по истечении таймаута.

Большинство из нас пишет такое уведомление поздно ночью — сразу после того, как отправка формы прошла успешно, а страница просто продолжает выглядеть так, будто ничего не произошло. У вас есть два надёжных пути: собрать легковесную систему самостоятельно с помощью стора writable и компонента-контейнера либо подключить поддерживаемую библиотеку. В этой статье рассматриваются оба варианта с готовым к копированию кодом, разбирается доступность и указываются места, где в старых руководствах по Svelte 4 используется синтаксис, ставший устаревшим в Svelte 5.

Всё изложенное ниже ориентировано на Svelte 5 — текущую стабильную мажорную версию, стабильную с октября 2024 года. Там, где Svelte 4 отличается, различия отмечены по ходу текста.

Ключевые выводы

  • В Svelte 5 стор для toast-уведомлений почти не меняется (writable([]) по-прежнему работает), но компонент toast приходится миграровать: export let заменяется на $props, on:click — на onclick, <slot /> — на сниппет, а createEventDispatcher — на callback-проп.
  • Чтобы скринридеры озвучивали toast-уведомления, отрисовывайте их внутри контейнера с aria-live (polite для информационных и успешных сообщений, assertive — для ошибок). role="alert" на каждом toast-уведомлении — это альтернатива такому контейнеру, а не дополнение к нему: их совместное использование может привести к тому, что одно сообщение будет озвучено дважды.
  • Присваивайте каждому toast-уведомлению защищённый от коллизий id через crypto.randomUUID() и сбрасывайте его таймер автозакрытия при удалении вручную, чтобы закрытое пользователем уведомление никогда не запускало устаревшее удаление.
  • svelte-sonner устанавливается командой npm i svelte-sonner, один раз отрисовывается как <Toaster /> в корне приложения, а затем вызывается откуда угодно через toast(), toast.success(), toast.error() или toast.promise().
  • Пишите своё решение, когда нужны нулевые зависимости и полный контроль; берите svelte-sonner, когда нужны promise-уведомления, закрытие свайпом, темизация и доступность «из коробки».

Что такое toast и когда его использовать?

Toast — это временная неблокирующая обратная связь: сообщения об успехе, ошибке или информационные уведомления, которые складываются в стек, автоматически закрываются по таймеру и никогда не прерывают пользователя так, как это делает модальное окно. Используйте toast, чтобы подтвердить отправку формы, показать асинхронную ошибку или подтвердить фоновое действие. Не используйте его для контента, на который пользователь обязан отреагировать или который не должен пропустить. Такому контенту место во встроенном сообщении или диалоге, поскольку toast может закрыться до того, как его прочитают.

Как построить систему toast-уведомлений в Svelte с использованием стора?

Основа самописной системы — единственный стор writable, хранящий массив объектов уведомлений, плюс хелперы addToast/dismissToast, которые можно вызывать откуда угодно. Сторы Svelte по-прежнему работают в Svelte 5, так что этот паттерн не устарел. Более новый подход с рунами в .svelte.ts идиоматичнее, но не обязателен.

// 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));
}

Здесь стоит обратить внимание на две детали корректности. Идентификаторы берутся из crypto.randomUUID(), а не из Math.random(), поэтому они не могут совпасть (метод работает только в защищённом контексте, то есть по HTTPS или на localhost). Кроме того, таймер каждого уведомления отслеживается в Map и сбрасывается при закрытии вручную, так что нажатие кнопки закрытия никогда не оставит setTimeout, указывающий на уже удалённое уведомление.

Теперь контейнер отрисовывает массив с ключом по id и передаёт каждому уведомлению колбэк закрытия:

<!-- 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>

Дочерний компонент Toast.svelte полностью использует идиомы Svelte 5: $props() для входных данных, onclick для события и callback-проп для закрытия:

<!-- 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>

Смонтируйте <Toasts /> один раз в корневом layout, а затем вызывайте откуда угодно:

import { addToast } from '$lib/toast-store.js';
addToast({ message: 'Saved!', type: 'success' });

Svelte 4 против Svelte 5: что изменилось в синтаксисе

Если вы копируете старое руководство с dev.to, стор перенесётся без изменений, а компонент — нет. В Svelte 5 export let заменён на $props, on:click превратился в атрибут onclick, а <slot /> заменён сниппетами. Самое главное — createEventDispatcher объявлен устаревшим: кнопка закрытия должна вызывать callback-проп (ondismiss?.()), а не отправлять событие. Версия Toast.svelte для Svelte 4 начиналась бы с export let type = 'info', import { createEventDispatcher } и использовала бы on:click={() => dispatch('dismiss')} — все эти паттерны в проекте на Svelte 5 считаются устаревшими.

Варианты оформления, позиционирование и доступность

Хороший toast от просто работающего отличают три UX-детали: варианты оформления, переходы и поддержка скринридеров. Варианты — это всего лишь поле type, сопоставленное с цветами фона (info/success/error), переход fade из svelte/transition анимирует появление и исчезновение, а контейнер с position: fixed и высоким z-index удерживает уведомления поверх страницы.

Доступность заслуживает отдельного разговора. Существует два способа добиться озвучивания toast-уведомления, и выбрать нужно строго один. role="alert" на каждом уведомлении подразумевает aria-live="assertive", и браузеры действительно обрабатывают alert-узлы особым образом: MDN отмечает, что их содержимое озвучивается в большинстве случаев, в том числе когда узел вставляется в страницу после загрузки. Загвоздка в том, что поведение зависит от связки «браузер + скринридер», поэтому постоянная live-область, которая уже присутствует в DOM, — более предсказуемый вариант. Именно поэтому контейнер в коде выше несёт aria-live="polite", а само уведомление не имеет роли. Используйте polite для информационных и успешных сообщений, чтобы объявления вставали в очередь за тем, что делает пользователь, и переключайте контейнер (или вторую область) на assertive для ошибок, требующих немедленного внимания.

Ошибка, которой следует избегать, — комбинировать оба подхода. MDN предупреждает, что совместное использование aria-live и role="alert" вызывает двойное озвучивание в VoiceOver на iOS, а assertive-alert, отрисованный внутри polite-области, провоцирует то же дублирование объявлений. Записи сессий с реализациями toast-уведомлений часто выявляют сценарий отказа, при котором уведомление появилось и автоматически закрылось, но так и не было воспринято: отсутствие live-области означало, что ничего не было озвучено.

Используйте библиотеку: svelte-sonner

svelte-sonner — путь «подключил и работает», и она создана под Svelte 5. Это порт библиотеки Sonner Эмиля Ковальского на Svelte, сохраняющий те же продуманные значения по умолчанию. Установите пакет, смонтируйте единственный <Toaster /> рядом с корнем приложения — и каждое уведомление, вызванное из любого места кодовой базы, будет отрисовано внутри него.

<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>

Плата за зависимость окупается методом toast.promise(), который открывается в состоянии загрузки, а затем сам заменяется сообщением об успехе или ошибке, как только промис завершается. Это единственный паттерн, который по-настоящему утомительно реализовывать вручную:

toast.promise(saveEvent(), {
  loading: 'Saving…',
  success: (data) => `${data.name} saved!`,
  error: 'Could not save'
});

<Toaster /> принимает пропы position, richColors, closeButton и duration, а для Tailwind вы стилизуете уведомления самостоятельно, передав объект toastOptions с unstyled: true и картой classes. Закрытие свайпом и клавиатурный фокус (⌥/alt + T) встроены изначально. npm i svelte-sonner устанавливает сборку версии 1.x; самая свежая запись в примечаниях к выпускам проекта — v1.1.1, где исправлен баг, из-за которого уведомления, настроенные никогда не исчезать, закрывались в момент обновления.

Две альтернативы. О svelte-french-toast стоит знать, но её опубликованный стабильный релиз относится к эпохе Svelte 4, поэтому пользователям Svelte 5 нужен форк вроде svelte-hot-french-toast. Второй вариант — @zerodevx/svelte-toast, актуальная ветка v0 которого объявляет peer-зависимости, охватывающие Svelte 3, 4 и 5.

Своё решение против svelte-sonner: как выбрать

Пишите своё, когда нужны нулевые зависимости, полный контроль над разметкой или желание разобраться в сторах Svelte; берите svelte-sonner, когда нужны promise-уведомления, закрытие свайпом, темизация и доступность «из коробки».

ПотребностьСвоё решениеsvelte-sonner
ЗависимостиНетОдин пакет
Контроль разметкиПолныйЧерез toastOptions (unstyled + classes)
Promise-уведомленияРеализуются вручнуюtoast.promise() встроен
Закрытие свайпомСвоими рукамиВстроено
Доступностьaria-live подключаете самиРеализована
Готовность к Svelte 5Да (с рунами/callback-пропами)Да, нативно

Среди библиотек svelte-sonner ориентирована непосредственно на Svelte 5; оригинальный svelte-french-toast относится к эпохе Svelte 4, а ветка v0 у @zerodevx/svelte-toast работает с Svelte 3, 4 и 5.

Начните с версии на сторе, если вам нужны сообщения об успехе, ошибке и информационные уведомления с автозакрытием. Это примерно 60 строк, и они научат вас паттерну сторов. В тот момент, когда потребуется обратная связь на основе промисов или жесты свайпа, установите svelte-sonner и удалите свой код. Что бы вы ни выбрали, сначала подключите область aria-live: это та деталь, которую легко пропустить и сложно заметить, когда её нет.

Часто задаваемые вопросы

Работает ли createEventDispatcher в Svelte 5?

Он ещё работает, но в Svelte 5 объявлен устаревшим, поэтому существующие компоненты с ним продолжают функционировать, а в новом коде его использовать не следует. Официальная замена для генерации событий вроде закрытия toast-уведомления — callback-проп: например, передать функцию ondismiss и вызывать ondismiss?.() из кнопки закрытия. В документации Svelte в качестве рекомендуемых альтернатив указаны callback-пропы и руна $host().

Стоит ли задавать role='alert' каждому уведомлению или использовать aria-live на контейнере?

Работает любой из подходов, но применять нужно один, а не оба сразу. Браузеры обрабатывают role='alert' особым образом и в большинстве случаев озвучивают его содержимое даже тогда, когда узел вставлен после загрузки страницы, хотя это зависит от связки браузера и скринридера. Постоянный контейнер, который уже присутствует в DOM и несёт aria-live, — более предсказуемый вариант: aria-live='polite' для информационных и успешных сообщений и 'assertive' для ошибок. Использование обоих подходов одновременно грозит дублированием объявления, и MDN отмечает, что комбинация aria-live и role='alert' вызывает двойное озвучивание в VoiceOver на iOS.

В чём разница между svelte-sonner и svelte-french-toast для Svelte 5?

svelte-sonner ориентирована непосредственно на Svelte 5 и устанавливается как сборка 1.x с promise-уведомлениями, закрытием свайпом, richColors и кнопкой закрытия. Опубликованная стабильная версия svelte-french-toast относится к эпохе Svelte 4, и её последний стабильный релиз вышел раньше Svelte 5, поэтому пользователям Svelte 5 нужен форк вроде svelte-hot-french-toast. Версия svelte-french-toast 2.0.0-alpha существует, но не вышла как стабильный релиз в npm.

Можно ли и дальше использовать стор writable для toast-уведомлений в Svelte 5 или нужно переходить на руны?

Стор writable по-прежнему работает в Svelte 5 и не считается устаревшим, поэтому стор toasts, построенный на writable([]) с хелперами добавления и закрытия, полностью корректен. Руны в файле .svelte.ts — более новый идиоматичный паттерн для общего реактивного состояния, но они не обязательны. Миграции на синтаксис Svelte 5 требует компонент, потребляющий стор, а не сам стор.

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.