Перейти к содержимому
Плотность
Тема

Обратная связь

Уведомление

JS Очередь, таймер и пауза под курсором — на kit.js. Вид, появление и уход — целиком CSS

Результат действия, которое запустил человек, — когда результату не место на экране, куда он смотрит. «Прогон поставлен в очередь», «настройки сохранены», «не удалось отправить».

Компонент
Прогон поставлен в очередь
worldbox-1 · седьмой в очереди
Настройки сохранены
Разметка
<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-toneneutral · running · ok · warn · errorЗначок и цвет. Без атрибута значка нет
data-stateleavingСтавит kit.js перед удалением, чтобы переход доиграл
rolestatus · alertalert только у ошибки: она перебивает, остальное сообщает вежливо
popovermanualНа области. Верхний слой, без лёгкого закрытия
токен
--surface-overlaylight-dark(var(--n-0), var(--n-11))Поверхность уведомления
--shadow-popover0 4px 12px -2px var(--shadow-color-far), 0 2px 4px -2px var(--shadow-color-near)Третий и последний носитель тени в ките

Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument