GrToaster
Берут, когда действие завершилось.
Обзор
Когда брать
- действие завершилось — «сохранено», «скопировано», «письмо отправлено»: подтверждение не должно занимать место;
- сообщение приходит откуда угодно — очередь живёт в
useToast, и вызвать её можно из любого модуля; - у сообщения есть отмена — кнопка действия в тосте заменяет диалог подтверждения на обратимом действии;
- сообщений может быть несколько — стек с очередью, паузой под курсором и прогрессом до закрытия.
Когда взять другое
| Нужно | Берите |
|---|---|
| Сообщение относится к месту на странице | GrAlert |
| Ошибка ответа сервера с деталями | GrResponseErrorBanner |
| Нужен ответ пользователя | GrConfirmDialog |
| Идёт длинный процесс | GrProgressBar |
На приложение ставится один GrToaster: состояние живёт в useToast, и
второй экземпляр показал бы ту же очередь дважды. Критичное сообщение тостом
не передают — он уйдёт сам, и пользователь может его не увидеть.
Playground контролы выводятся из API, а не пишутся руками
Этот компонент живёт поверх экрана, а не внутри холста: его открывает экран, а конструктору открывать его неоткуда. Примеры ниже показывают его в работе.
0 изменено· состояние — в адресе
<GrToaster>Текст</GrToaster>Примеры 3 у каждого — живое демо и его код
Что остаётся после действия со счётом
Напоминание подтверждается и уходит само. Аннулирование обратимо, поэтому диалога подтверждения нет: тост несёт «Undo» и держится восемь секунд, чтобы им успели воспользоваться. PDF — один тост на весь запрос:
promiseпереписывает «Preparing» в результат на месте, а сам промис возвращается как есть — отказ по-прежнему ловим мы.INV-2026-0418 · Northwind Traders€1,284.50 · due 1 NovemberOpenСканер, который теряет сеть
Сокет повторяет попытку каждую секунду и сообщает о каждой неудаче;
dedupeKeyсворачивает повторы в один живой тост и перезапускает его таймер, а тост «снова в сети» встаёт на его место под тем же ключом. Пять сканов — пять подтверждений: с третьего стек складывается в стопку черезcollapse,max-visibleограничивает экран, а слотactionsдаёт предупреждению «Retry now», а принятой коробке — «Undo scan».Receiving · Dock 3Online — scans are sent at once Дотянуться до тоста с клавиатуры
Выгрузка заканчивается, пока пользователь глубоко в длинной странице.
focus-hotkey— по умолчанию F6, клавиша APG для перехода между регионами, — переводит в стек откуда угодно, аfocus()делает то же с кнопки и сообщает, было ли что фокусировать. Delete закрывает тост под фокусом — клавиатурный двойник смахивания, которое здесь выключено черезswipe-dismiss.Customers · export
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
placement | GrToasterPlacement | undefined | "top-right" | Угол экрана для стека уведомлений. |
regionLabel | string | undefined | undefined | A11y-лейбл контейнера-региона (i18n). |
collapse | number | false | undefined | 3 | Со скольких видимых тостов стек схлопывается в стопку: колонка занимает место одного тоста, а остальные выглядывают из-под передней. Разворачивается по наведению и по фокусу. `false` — не схлопывать никогда. Порог, а не переключатель: пока тостов мало, колонка читается лучше стопки, и менять её незачем. |
width | string | number | undefined | undefined | Ширина стека. Число — пиксели, строка — любая CSS-длина. Уезжает в `--gr-toaster-width`, поэтому то же самое можно задать и темой. |
dismissLabel | string | undefined | undefined | A11y-лейбл кнопки закрытия (i18n). |
maxVisible | number | undefined | 4 | Максимум одновременно видимых тостов; остальные ждут в очереди. По умолчанию `4`. |
focusHotkey | string | false | undefined | "F6" | Клавиша перевода фокуса в стек уведомлений; `false` отключает хоткей. `F6` — рекомендация APG для перехода между «регионами» страницы. |
swipeDismiss | boolean | undefined | true | Смахивание тоста к своему краю экрана. Клавиатурный эквивалент — `Delete` и `Backspace` на сфокусированном тосте — остаётся и при выключенном жесте. |
Слоты
| Slot | Type | Описание |
|---|---|---|
actions | { toast: Toast; dismiss: () => void; } | Кнопки тоста вместо `action`/`actions` из его описания. Получает сам `toast` и `dismiss` — функцию, закрывающую именно этот тост. |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
focus | () => boolean | Фокус на верхний тост, как по `focusHotkey`; `false` — тостов нет. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrToasterPlacement, GrToasterProps, GrToastTone } from '@feugene/granularity/components/GrToaster'Файлdist/types/components/GrToaster/grToasterStyles.d.ts
type GrToasterPlacement = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';Файлdist/types/components/GrToaster/GrToaster.vue.d.ts
interface GrToasterProps {
/** Угол экрана для стека уведомлений. */
placement?: GrToasterPlacement;
/** Максимум одновременно видимых тостов; остальные ждут в очереди. По умолчанию `4`. */
maxVisible?: number;
/** A11y-лейбл кнопки закрытия (i18n). */
dismissLabel?: string;
/** A11y-лейбл контейнера-региона (i18n). */
regionLabel?: string;
/**
* Ширина стека. Число — пиксели, строка — любая CSS-длина. Уезжает в
* `--gr-toaster-width`, поэтому то же самое можно задать и темой.
*/
width?: string | number;
/**
* Клавиша перевода фокуса в стек уведомлений; `false` отключает хоткей.
* `F6` — рекомендация APG для перехода между «регионами» страницы.
*/
focusHotkey?: string | false;
/**
* Смахивание тоста к своему краю экрана. Клавиатурный эквивалент — `Delete`
* и `Backspace` на сфокусированном тосте — остаётся и при выключенном жесте.
*/
swipeDismiss?: boolean;
/**
* Со скольких видимых тостов стек схлопывается в стопку: колонка занимает
* место одного тоста, а остальные выглядывают из-под передней. Разворачивается
* по наведению и по фокусу. `false` — не схлопывать никогда.
*
* Порог, а не переключатель: пока тостов мало, колонка читается лучше стопки,
* и менять её незачем.
*/
collapse?: number | false;
}Файлdist/types/components/GrToaster/config.d.ts
const grToasterConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrToaster">;Файлdist/types/components/GrToaster/safelist.d.ts
const grToasterSafelist: string[];Файлdist/types/composables/useToast.d.ts
type GrToastTone = GrTone;Опциональная action-кнопка тоста: label — текст, onClick — обработчик.
По клику вызывается onClick, затем тост закрывается (если dismissOnClick
не установлен в false). size/variant настраивают вид кнопки.
type ToastAction = {
label: string;
onClick: () => void;type ToastInput = {
title: string;
message?: string;
tone?: GrToastTone;
/**
* Автозакрытие через N мс. `0` или отрицательное значение — «навсегда»
* (до явного вызова `dismiss(id)` / `clear()`). По умолчанию: `3500`.
*/
timeoutMs?: number;
/** Одна action-кнопка в теле тоста (шорткат для `actions: [action]`). */
action?: ToastAction;
/** Несколько action-кнопок в теле тоста. Рендерятся после `action`. */
actions?: ToastAction[];
/**
* Схлопывать повтор: `push` с уже занятым ключом обновляет **живой** тост
* и перезапускает его автозакрытие вместо того, чтобы завести второй.
*
* Ключ действует только пока тост на экране. Это не то же самое, что помнить
* последнее показанное: повтор действия с тем же текстом обязан показаться
* снова, иначе второе «Сохранено» пропадёт молча.
*/
dedupeKey?: string;
};type Toast = {
id: string;
title: string;
message?: string;
tone: GrToastTone;
/** Исходный таймаут автозакрытия, мс (`0` — без автозакрытия). Нужен для progress-бара. */
timeoutMs: number;
/** Одна action-кнопка в теле тоста. */
action?: ToastAction;
/** Несколько action-кнопок в теле тоста. */
actions?: ToastAction[];
/** Ключ схлопывания повторов, если тост создан с ним. */
dedupeKey?: string;
};Файлdist/types/components/GrToaster/index.d.ts
type GrToasterInstance = ComponentExposed<typeof GrToasterComponent>;Доступность
| Клавиша | Действие |
|---|---|
| — | F6 (проп focusHotkey) — фокус на верхний тост, дальше действия обходятся Tab; сам тост остановкой Tab не является; Delete/Backspace на сфокусированном тосте закрывают его — клавиатурный эквивалент смахивания |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Запрос одним тостом
toast.promise ведёт весь жизненный цикл запроса в одном тосте: «загружаем»
переписывается в результат, а не закрывается ради нового — стек не дёргается.
const { promise } = useToast()
promise(api.sync(), {
loading: { title: 'Синхронизируем', message: 'Отправляем изменения…' },
success: result => ({ title: 'Готово', message: `${result.files} файлов` }),
error: reason => ({ title: 'Не вышло', message: String(reason) }),
})Строка вместо объекта — шорткат для { title }. Загрузочный тост показывается
без автозакрытия, результат получает обычный таймаут.
Промис возвращается как есть и отказ не глотается — тост не заменяет
обработку ошибки, catch по-прежнему на вызывающем. Если пользователь закрыл
тост руками, пока запрос шёл, результат его не воскресит.
Точечно тост меняется через update(id, patch): он же перезапускает таймер,
когда в патче приходит timeoutMs.
Сколько тостов помнит очередь
maxVisible у компонента ограничивает только видимые тосты. Сама очередь
ограничена потолком состояния — по умолчанию 20; при переполнении вытесняются
самые старые, вместе с их таймерами. Иначе поток событий (переподключение
сокета, цикл ошибок) копил бы очередь и вываливал её на пользователя, когда
стек освободится.
app.use(granularityToastPlugin, { maxToasts: 50 })Стопка вместо колонки
maxVisible держит число видимых тостов, но ничего не говорит про занятое ими
место: подняв его до восьми, приложение получало колонку в пол-экрана поверх
того интерфейса, ради которого уведомления и показывают.
collapse (по умолчанию 3) задаёт, со скольких видимых тостов стек
схлопывается в стопку: передняя карточка видна целиком, из-под неё выглядывают
края следующих, и весь стек занимает место одного тоста. collapse: false
выключает схлопывание совсем.
Порог, а не переключатель: пока тостов два-три, колонка читается лучше стопки — все сообщения видны сразу, и прятать их незачем.
Разворачивают стопку наведение и фокус — те же два сигнала, что ставят таймеры на паузу (см. «Пауза автозакрытия»). Раз уж читать стек начали, отсчёт всё равно обязан остановиться, и заводить для разворота третий сигнал значило бы развести эти два события во времени.
Стопка растёт от своего угла вглубь экрана: у верхних углов вниз, у нижних — вверх. Содержимое карточек за передней погашено: из щели в двенадцать пикселей видны края карточек, а не обрывки строк.
| Токен | Дефолт | Что меняет |
|---|---|---|
--gr-toaster-stack-peek | 12px | насколько выглядывает каждая следующая карточка |
--gr-toaster-stack-scale | 0.05 | на сколько она уменьшается |
Очередь этим не заменяется: стопка — про раскладку видимых тостов, очередь — про то, скольким вообще дают показаться.
Повтор схлопывается ключом, а не запоминанием текста
dedupeKey у push() заменяет живой тост с тем же ключом и перезапускает его
автозакрытие. Типовой источник дублей — навигация, переигрывающая одни и те же
пропсы страницы: без ключа каждое повторение заводило бы новый тост.
push({ title: flash.success, tone: 'success', dedupeKey: `flash:${flash.success}` })Ключ занят, только пока тост на экране. Это не то же самое, что помнить
последний показанный текст: такая память не сбрасывается, и повтор действия с
тем же текстом («Сохранено» второй раз) не показался бы вовсе. Закрылся тост —
ключ свободен, и следующий push снова видимый.
Повтор заменяет тост целиком, как стадия promise(): message и кнопки от
предыдущего вызова не донашиваются. Точечная правка показанного тоста — это
update(id, patch), у неё патч-семантика.
Объявление тостов скринридеру
Тост объявляет себя сам: role="status", а warning/danger — ассертивный
role="alert". Постоянной обёртки-live-region над списком нет и не должно
быть: вложение регионов с разной ассертивностью спецификацией не определено —
браузеры и скринридеры расходятся вплоть до полной потери объявления.
Контейнер остаётся именованным role="region" (regionLabel), это навигация,
а не объявление.
Смахивание
Тост смахивается к своему краю экрана: стек справа уходит вправо, слева — влево. Смахивание вглубь экрана читалось бы как попытка достать что-то из-под тоста, а не выбросить его, поэтому движение «наоборот» идёт с сопротивлением — тост едет за пальцем, но не закрывается.
Порог — четверть ширины тоста, но не меньше 56px. Отпустили раньше — тост вернулся; отпустили дальше — закрылся. Обрыв жеста возвращает тост: если указатель забрал браузер (системный жест, звонок, потеря окна), пользователь жест не довёл, и дожимать его за него нельзя.
Пока тост тянут, его таймер стоит — иначе уведомление сгорало бы прямо под пальцем, посреди жеста.
Отпущенный тост уезжает за свой край и только потом закрывается. Отлёт — это тот
же переход, что и у остальных состояний тоста, поэтому prefers-reduced-motion
чинить нечем и не нужно: глобальный кламп сжимает переход до мгновенного, а сам
жест — прямое манипулирование, а не движение интерфейса, и гасить его нечего.
swipe-dismiss="false" выключает жест; клавиатурный эквивалент при этом
остаётся.
Клавиатура
Тосты телепортированы в конец body, поэтому кнопка «Отменить» лежит за
пределами разумного числа нажатий Tab. F6 (проп focusHotkey, false
отключает) переводит фокус на верхний тост, дальше действия обходятся обычным
Tab. Сам тост собственной остановкой Tab не является — tabindex="-1":
всплывающее уведомление не должно перехватывать обход страницы.
То же снаружи — через ref:
<GrToaster ref="toaster" />focus() возвращает false, если стек пуст.
Delete и Backspace на сфокусированном тосте закрывают его — то же действие,
что смахивание. Фокус переходит на соседний тост, если он есть: падать на body
посреди стека, который ещё читают, некуда. Закрыт последний — фокус
возвращается туда, откуда пришли в стек (F6 или focus() запоминают этот
элемент), если он ещё в документе. Так же ведут себя крестик и кнопки действий
с dismissOnClick, когда фокус на них. Escape намеренно не занят — тостер не
модальный слой, и перехват закрывал бы уведомление вместо диалога под ним.
Ширина
width — число (пиксели) или CSS-длина; уезжает в --gr-toaster-width,
поэтому то же самое задаётся и темой. Дефолт — 360px, потолок —
calc(100vw - 2rem).
Пауза автозакрытия
Курсор, фокус внутри стека или начатое смахивание останавливает таймеры всех
видимых тостов и замораживает полосу прогресса (WCAG 2.2.1). Тосты из очереди (сверх
maxVisible) стоят на паузе всегда — отсчёт начинается, когда тост становится
видимым.
Пропы этого примера