ToastSystemEvent
Системные уведомления о состоянии системы — успех/ошибка/предупреждение, опционально с link или action-кнопками. По умолчанию auto-dismiss 5s, в stacked-контейнере накапливается в deck-стопку.
Когда использовать
- Сообщить о результате асинхронной операции (сохранение, отправка, синхронизация).
- Информировать о системных событиях, которые не блокируют UI (фоновые задачи, networking).
- Показать критическую ошибку с возможностью повторить (
appearance='errorCritical'+ action).
Когда не нужен SystemEvent:
- Короткий ack на пользовательское действие («скопировано», «отменено») — используй UserAction.
- Прогресс загрузки файлов — используй Upload.
Анатомия
Appearance (default neutral)
neutral— информационное (дефолт).success— успешное завершение действия.warning— предупреждение, требующее внимания.error— обычная ошибка.errorCritical— критическая ошибка с акцентом (красный фон, изменённый стиль link).
Progress bar (default true)
Полоса auto-dismiss таймера внизу карточки. Анимация совпадает с autoClose мс. Pause при hover контейнера и в свёрнутом стеке (ghost-карточки).
Closable (default true)
Крестик закрытия в правом верхнем углу. Клик вызывает onCloseClick(e, closeToast); если хендлер не задан — вызывается closeToast.
Action (default undefined)
Массив 1–2 кнопок в footer’е. По соглашению Figma primary-кнопка находится справа, поэтому первый элемент массива (action[0]) рендерится справа как primary action, второй (action[1]) — слева как secondary. Используется для критических подтверждений (autoClose: false рекомендуется).
Установка
import { toaster, ToasterContainer, TOASTER_TYPE } from '@cloud-ru/ds-toaster'
Примеры использования
Базовые appearance
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_SYSTEM_EVENT, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';
const CONTAINER_ID = 'demo-basic-system-event';
export function BasicSystemEvent() {
return (
<PortalContextProvider>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Button
appearance={APPEARANCE.Primary}
label='Success'
onClick={() =>
toaster.systemEvent.success({
title: 'Изменения сохранены',
description: 'Все данные синхронизированы',
containerId: CONTAINER_ID,
})
}
/>
<Button
appearance={APPEARANCE.Critical}
label='Error'
onClick={() =>
toaster.systemEvent.error({
title: 'Не удалось сохранить',
description: 'Попробуйте ещё раз',
link: { label: 'Подробнее', href: '#' },
containerId: CONTAINER_ID,
})
}
/>
<Button
appearance={APPEARANCE.Neutral}
label='Закрыть все'
onClick={() => toaster.systemEvent.dismiss({ containerId: CONTAINER_ID })}
/>
<ToasterContainer
type={TOASTER_TYPE.SystemEvent}
containerId={CONTAINER_ID}
position={POSITION_SYSTEM_EVENT.BottomRight}
limit={5}
autoClose={5000}
stacked={false}
displayCloseAllButton={false}
/>
</div>
</PortalContextProvider>
);
}Stack-режим
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_SYSTEM_EVENT, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';
import { useRef } from 'react';
const CONTAINER_ID = 'demo-stacked-system-event';
export function StackedSystemEvent() {
const counter = useRef(0);
const fire = () => {
counter.current += 1;
toaster.systemEvent.neutral({
title: `Уведомление #${counter.current}`,
description: 'Hover по стопке разворачивает её',
containerId: CONTAINER_ID,
});
};
return (
<PortalContextProvider>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Button appearance={APPEARANCE.Primary} label='Добавить тост' onClick={fire} />
<Button
appearance={APPEARANCE.Neutral}
label='Закрыть все'
onClick={() => toaster.systemEvent.dismiss({ containerId: CONTAINER_ID })}
/>
<ToasterContainer
type={TOASTER_TYPE.SystemEvent}
containerId={CONTAINER_ID}
position={POSITION_SYSTEM_EVENT.BottomRight}
limit={5}
autoClose={5000}
stacked
displayCloseAllButton
/>
</div>
</PortalContextProvider>
);
}С action-кнопками
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_SYSTEM_EVENT, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';
const CONTAINER_ID = 'demo-system-event-with-action';
export function SystemEventWithAction() {
const dismiss = () => toaster.systemEvent.dismiss({ containerId: CONTAINER_ID });
// `autoClose: false` — тост висит, пока пользователь не нажмёт action или
// close. Используется, когда от пользователя ждут явного решения.
const openSticky = () =>
toaster.systemEvent.warning({
title: 'Подтвердите действие',
description: 'Удалить этот файл без возможности восстановления?',
action: [
{ label: 'Удалить', onClick: dismiss },
{ label: 'Отмена', onClick: dismiss },
],
autoClose: false,
containerId: CONTAINER_ID,
});
// Без `autoClose: false` — тост закроется по таймеру контейнера (autoClose
// = 5000ms). Action остаётся как «удобный сокращённый путь», но решение
// не блокирующее.
const openAutoClosing = () =>
toaster.systemEvent.warning({
title: 'Файл будет удалён через 5 секунд',
description: 'Нажмите «Отменить», чтобы прервать.',
action: [{ label: 'Отменить', onClick: dismiss }],
containerId: CONTAINER_ID,
});
return (
<PortalContextProvider>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Button appearance={APPEARANCE.Critical} label='Sticky (autoClose: false)' onClick={openSticky} />
<Button
appearance={APPEARANCE.Primary}
label='С таймером (autoClose из контейнера)'
onClick={openAutoClosing}
/>
<Button appearance={APPEARANCE.Neutral} label='Закрыть все' onClick={dismiss} />
<ToasterContainer
type={TOASTER_TYPE.SystemEvent}
containerId={CONTAINER_ID}
position={POSITION_SYSTEM_EVENT.BottomRight}
limit={5}
autoClose={5000}
stacked={false}
displayCloseAllButton={false}
/>
</div>
</PortalContextProvider>
);
}Обновление: loading → success / error
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_SYSTEM_EVENT, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';
const CONTAINER_ID = 'demo-system-event-saving-flow';
const PENDING_DELAY_MS = 1500;
export function SystemEventSavingFlow() {
const run = async () => {
const id = await toaster.systemEvent.neutral({
title: 'Сохранение…',
description: 'Не закрывайте окно',
autoClose: false,
progressBar: false,
closable: false,
containerId: CONTAINER_ID,
});
setTimeout(() => {
toaster.systemEvent.update.success(id, {
title: 'Сохранено',
description: 'Все изменения зафиксированы',
autoClose: 3000,
containerId: CONTAINER_ID,
});
}, PENDING_DELAY_MS);
};
const fail = async () => {
const id = await toaster.systemEvent.neutral({
title: 'Отправка…',
description: 'Ждём ответа сервера',
autoClose: false,
progressBar: false,
closable: false,
containerId: CONTAINER_ID,
});
setTimeout(() => {
toaster.systemEvent.update.error(id, {
title: 'Ошибка соединения',
description: 'Попробуйте ещё раз',
autoClose: 5000,
closable: true,
containerId: CONTAINER_ID,
});
}, PENDING_DELAY_MS);
};
return (
<PortalContextProvider>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Button appearance={APPEARANCE.Primary} label='Сохранить → success' onClick={run} />
<Button appearance={APPEARANCE.Critical} label='Сохранить → error' onClick={fail} />
<Button
appearance={APPEARANCE.Neutral}
label='Закрыть все'
onClick={() => toaster.systemEvent.dismiss({ containerId: CONTAINER_ID })}
/>
<ToasterContainer
type={TOASTER_TYPE.SystemEvent}
containerId={CONTAINER_ID}
position={POSITION_SYSTEM_EVENT.BottomRight}
limit={3}
autoClose={5000}
stacked={false}
displayCloseAllButton={false}
/>
</div>
</PortalContextProvider>
);
}Положения контейнера
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_SYSTEM_EVENT, SystemEventPosition, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';
const POSITIONS: SystemEventPosition[] = Object.values(POSITION_SYSTEM_EVENT);
const containerIdFor = (position: SystemEventPosition) => `demo-system-event-position-${position}`;
export function SystemEventPositions() {
const fire = (position: SystemEventPosition) =>
toaster.systemEvent.neutral({
title: `Тост в ${position}`,
description: 'Каждое положение — свой контейнер',
containerId: containerIdFor(position),
});
const dismissAll = () =>
POSITIONS.forEach(position => toaster.systemEvent.dismiss({ containerId: containerIdFor(position) }));
return (
<PortalContextProvider>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
{POSITIONS.map(position => (
<Button key={position} appearance={APPEARANCE.Primary} label={position} onClick={() => fire(position)} />
))}
<Button appearance={APPEARANCE.Neutral} label='Закрыть все' onClick={dismissAll} />
{POSITIONS.map(position => (
<ToasterContainer
key={position}
type={TOASTER_TYPE.SystemEvent}
containerId={containerIdFor(position)}
position={position}
limit={3}
autoClose={4000}
stacked={false}
displayCloseAllButton={false}
/>
))}
</div>
</PortalContextProvider>
);
}Хуки закрытия: onCloseClick + onClose
- Лог пуст
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_SYSTEM_EVENT, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';
import { useState } from 'react';
import styles from './styles.module.scss';
const CONTAINER_ID = 'demo-system-event-close-hooks';
export function SystemEventCloseHooks() {
const [log, setLog] = useState<string[]>([]);
const push = (line: string) => setLog(prev => [line, ...prev].slice(0, 4));
const open = () => {
toaster.systemEvent.warning({
title: 'Несохранённые изменения',
description: 'Закрыть тост?',
containerId: CONTAINER_ID,
onCloseClick: (_e, close) => {
if (window.confirm('Закрыть уведомление?')) {
push('onCloseClick → close()');
close?.();
} else {
push('onCloseClick → отменено');
}
},
onClose: id => push(`onClose(${String(id)})`),
});
};
return (
<PortalContextProvider>
<div className={styles.uploadFrame}>
<Button appearance={APPEARANCE.Primary} label='Показать тост' onClick={open} />
<Button appearance={APPEARANCE.Neutral} label='Очистить лог' onClick={() => setLog([])} />
<ul className={styles.eventLog} aria-label='Лог событий'>
{log.length === 0 ? <li>Лог пуст</li> : log.map((line, i) => <li key={i}>{line}</li>)}
</ul>
<ToasterContainer
type={TOASTER_TYPE.SystemEvent}
containerId={CONTAINER_ID}
position={POSITION_SYSTEM_EVENT.BottomRight}
limit={3}
autoClose={false}
/>
</div>
</PortalContextProvider>
);
}Императивный API
Тосты создаются через toaster.systemEvent.<appearance>({...}) — один метод на каждый visual вариант. Обновление существующего тоста — toaster.systemEvent.update.<appearance>(id, {...}). Закрытие — toaster.systemEvent.dismiss(...).
import { toaster, type SystemEventOptions } from '@cloud-ru/ds-toaster'
const id = await toaster.systemEvent.error({
id: 'save-error',
containerId: 'main',
title: 'Не удалось сохранить',
description: 'Сервер вернул 500',
link: { label: 'Подробнее', href: '/logs/123' },
autoClose: false,
onCloseClick: (_e, close) => confirm('Закрыть?') && close?.(),
onClose: id => log('closed', id),
})
toaster.systemEvent.update.warning(id, {
title: 'Сохранение временно недоступно',
description: 'Повтор через 30s',
})
toaster.systemEvent.dismiss(id)
Props
Types
ToastSystemEventProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
action | ToastButtonActionProps[] | — | no | Кнопки в footer'е |
appearance | "error" | "errorCritical" | "neutral" | "success" | "warning" | neutral | no | Цветовая схема |
autoClose | number | false | undefined (резолвится в `AUTO_CLOSE_TIME[TOASTER_TYPE.SystemEvent]` = 5000) | no | Время автозакрытия в мс. Три состояния различимы намеренно: - `undefined` — потребитель не указал значение, toaster-система подставит дефолт `AUTO_CLOSE_TIME[TOASTER_TYPE.SystemEvent]` (5000 мс) при рендере; именно этот случай позволяет ProgressBar анимироваться синхронно с auto-dismiss таймером менеджера. - `number` — явное значение от потребителя, используется как есть. - `false` — автозакрытие выключено, ProgressBar в этом режиме не рендерится. |
className | string | — | no | Доп. класс корневого элемента |
closable | boolean | true | no | Возможность закрыть тост |
closeToast | (() => void) | — | no | |
data-test-id | string | — | no | |
description | string | — | no | Описание под заголовком |
link | ToastSystemEventLink | — | no | Slot для ссылки |
onCloseClick | ((e: MouseEvent<HTMLButtonElement, MouseEvent>, close?: (() => void)) => void) | — | no | Кастомный обработчик клика по кнопке закрытия |
progressBar | boolean | true | no | Показывать ли ProgressBar внизу |
title | string | — | yes | Заголовок тоста |
toastProps | { autoClose?: number | false; } | — | no |
Unions
Types
ToastSystemEventProps
ToastButtonActionProps
ToastSystemEventLink
Unions
ToastSystemEventAppearance
Storybook
Playground — все props через controls (appearance, link, action, closable, autoClose):
VisualMatrix · все варианты в одной сетке
Все 5 appearance × состояния (с/без link, с/без action, closable) — удобно для визуальной сверки и регрессии.
Связанные стори контейнера:
- Stacking — поведение стека из SystemEvent-тостов:
components-toaster-toaster--visual-matrix. - Triggers — императивные шорткаты
toaster.systemEvent.*:components-toaster-toaster--imperative-api. - Positions — все 6 точек якоря:
components-toaster-toaster--visual-matrix.
Figma
Смотри также
- Toaster (root) — обзор пакета.
- UserAction — короткие ack-уведомления.