Обратная связь
Уведомление
Результат действия, которое запустил человек, — когда результату не место на экране, куда он смотрит. «Прогон поставлен в очередь», «настройки сохранены», «не удалось отправить».
Разметка
<div class="inst-toasts" style="position:static;inset:auto;pointer-events:auto">
<div class="inst-toast" data-tone="ok" role="status">
<div class="inst-toast-body">
<div class="inst-toast-title">Прогон поставлен в очередь</div>
<div class="inst-toast-text">worldbox-1 · седьмой в очереди</div>
</div>
<div class="inst-toast-actions">
<button class="inst-btn inst-btn--sm inst-btn--ghost" type="button">Показать</button>
</div>
</div>
<div class="inst-toast" data-tone="error" role="alert">
<div class="inst-toast-body">
<div class="inst-toast-title">Не удалось отправить</div>
<div class="inst-toast-text">Сеть недоступна. Попытка 3 из 5.</div>
</div>
</div>
<div class="inst-toast">
<div class="inst-toast-body">
<div class="inst-toast-title">Настройки сохранены</div>
</div>
</div>
</div>
Инлайновый стиль в примере — единственный на весь справочник, и он тут вынужденный: настоящая область живёт в верхнем слое и прижата к углу экрана, а показать её надо внутри кадра. В приложении так писать не нужно.
Использование
Разметку строит кит: тост заводится вызовом, а не написанным вручную узлом.
Если стандартной не хватает, соберите свой и положите в .inst-toasts сами.
| Что | Обязательно | Почему |
|---|---|---|
role="status", у ошибки role="alert" |
да | Ошибка перебивает, остальное сообщает вежливо |
duration: 0 у ошибки |
да | Сообщение о том, что действие не выполнено, не имеет права уйти незамеченным |
| Одно действие, не два | да | Тост уходит сам; выбор из двух вариантов требует времени, которого у него нет |
| Область одна на документ | да | popover="manual", открыта с инициализации |
Когда использовать
| Используйте | Возьмите другое |
|---|---|
| Результат действия, которое человек запустил сам | Состояние экрана — баннер: «идёт обслуживание» держится, пока держится состояние, и уходить само не должно |
| Подтверждение, за которым не нужно следить | Отказ, из которого нужен выход — блок отказа: у него есть причина и действие, и он остаётся в потоке |
| Отмена только что сделанного | Пояснение к тому, что рядом — сноска |
| Ошибка, не мешающая продолжать | Вопрос, без ответа на который нельзя дальше — модалка |
Поведение
Почему popover
Область объявлена popover, потому что z-index не решает задачу целиком.
Тост обязан быть виден поверх всего, что нарисовало приложение: панели с
overflow: hidden, липкой шапки, открытого поповера. Верхний слой даёт это
по построению, а число в z-index — только до первого чужого числа побольше.
Отсюда же в ките нет отдельного z-индекса для тостов: применить его было бы некуда.
Модалка выше тоста
Тост не перекрывает открытую модалку. Проверено замером: область,
переоткрытая после showModal() — и в той же задаче, и через задачу, —
остаётся под диалогом. Верхний слой упорядочен по времени входа, но
модальный диалог держится выше манипуляций с popover.
Что делать вместо:
| Ситуация | Как |
|---|---|
| Действие запущено из модалки и она закрывается | Закройте диалог, потом покажите тост |
| Модалка остаётся открытой | Результат — внутри неё: сноска или блок отказа в теле диалога |
| Долгая операция при открытой модалке | Мера внутри диалога, а тост — после закрытия |
Так даже правильнее: сообщение о результате действия, запущенного в диалоге, и должно приходить туда, куда смотрит человек.
Почему область открыта всегда
popover="manual" открывается один раз при инициализации и не закрывается.
Закрытый popover — это display: none, а живой регион в display: none
не озвучивается: скринридер не сообщил бы ни одного уведомления. Пустая
открытая область невидима и ничего не перехватывает — pointer-events
возвращаются только самим тостам.
Пауза и потолок очереди
Таймер замирает под курсором и на фокусе. Уведомление, исчезнувшее тогда, когда его начали читать, — это потерянное сообщение, и WCAG 2.2.1 требует этого: у времени должна быть пауза.
Больше четырёх одновременно не показывается — двадцать уведомлений подряд читаются как стена. Самые старые уходят, чтобы новое было видно.
JS
Тост не пишут разметкой — его зовут. Поэтому пример здесь один и он живой: нажмите, и уведомление придёт в правый нижний угол экрана.
Разметка
<button class="inst-btn inst-btn--primary" type="button"
data-demo-toast='{"tone":"ok","title":"Прогон поставлен в очередь","text":"worldbox-1 · седьмой в очереди"}'>Успех</button>
<button class="inst-btn" type="button"
data-demo-toast='{"tone":"error","title":"Не удалось отправить","duration":0}'>Ошибка, которая не уходит</button>
<button class="inst-btn" type="button"
data-demo-toast='{"tone":"running","title":"Идёт сборка","text":"Осталось два шага"}'>Идёт</button>
data-demo-toast — атрибут этого сайта, а не кита: он нужен, чтобы
страница осталась разметкой. В приложении вы вызываете toast() из кода.
import { toast } from './src/kit.js';
toast({ tone: 'ok', title: 'Прогон поставлен в очередь',
text: 'worldbox-1 · седьмой в очереди' });
Без скрипта тостов нет вовсе: очередь, таймер и пауза под курсором — работа, которую CSS не выполняет. Вид, появление и уход при этом целиком на CSS.
Методы
toast(options) |
Показать уведомление. Возвращает узел — его можно убрать раньше срока |
Опции
| Поле | Что делает |
|---|---|
title |
Что произошло. Одна строка, без точки |
text |
Подробность. Необязательна |
tone |
ok · warn · error · running · neutral |
duration |
Миллисекунды. 0 — не уходит сам |
action |
{ label, onClick } — одно действие, не два |
toast({ tone: 'error', title: 'Не удалось отправить', duration: 0 });
toast({ title: 'Задача удалена',
action: { label: 'Вернуть', onClick: () => restore() } });
События
Своих нет. Действие приходит колбэком action.onClick.
Правила
Так duration: 0 у ошибки
Сообщение о невыполненном действии не имеет права уйти незамеченным.
Не так Тост для состояния экрана
«Идёт обслуживание» держится, пока держится состояние. Уходящее само сообщение об этом соврёт — нужен баннер.
Так Одно действие в тосте
«Отменить» или «Показать». Тост уходит сам, и на выбор из двух вариантов времени нет.
Не так Тост поверх открытой модалки
Он под ней не виден. Результат действия из диалога сообщается внутри диалога или после его закрытия.
Доступность
| Роль | status у обычного, alert у ошибки. Ошибка перебивает, остальное сообщает вежливо |
| Живой регион | Область существует и открыта до появления первого тоста. Регион, созданный одновременно с содержимым, не озвучивается |
| Пауза | Таймер замирает под курсором и на фокусе — WCAG 2.2.1 |
| Ошибка | duration: 0: сообщение о невыполненном действии не уходит само |
| Не только цвет | Тон несёт значок, а не одну заливку — тот же набор масок, что у сноски и баннера |
| Уменьшенное движение | Переход схлопывается; удаление подстраховано таймером, иначе тост остался бы навсегда |
| Клавиатура | Действие внутри — обычная кнопка и достижимо Tab. Область не перехватывает фокус: она сообщает, а не спрашивает |
| Печать | Уведомления на листе не печатаются: они по определению временные |
| Модалка | Тост под ней не виден. Сообщайте результат внутри диалога или после закрытия — см. выше |
API
| Имя | Значение | Что делает |
|---|---|---|
| класс | ||
inst-toasts | — | Область. Одна на документ, объявлена popover="manual" |
inst-toast | — | Одно уведомление |
inst-toast-body | — | Текстовая часть |
inst-toast-title | — | Что произошло. Одна строка |
inst-toast-text | — | Подробность. Необязательна |
inst-toast-actions | — | Действие: «Отменить», «Показать» |
| модификатор | ||
inst-toasts--start | — | Область у верхнего края. Умолчание — нижний |
| атрибут | ||
data-tone | neutral · running · ok · warn · error | Значок и цвет. Без атрибута значка нет |
data-state | leaving | Ставит kit.js перед удалением, чтобы переход доиграл |
role | status · alert | alert только у ошибки: она перебивает, остальное сообщает вежливо |
popover | manual | На области. Верхний слой, без лёгкого закрытия |
| токен | ||
--surface-overlay | light-dark(var(--n-0), var(--n-11)) | Поверхность уведомления |
--shadow-popover | 0 4px 12px -2px var(--shadow-color-far), 0 2px 4px -2px var(--shadow-color-near) | Третий и последний носитель тени в ките |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument