Toast
A toast reports something that has already happened. Deck's are a
stack rather than a list: new ones arrive at the front, older ones
collapse behind, and hovering the stack fans them out and pauses every timer in it.
You almost never write the markup — Deck.toast() builds it.
When to use it
Use a toast for the result of something the reader just did, when they do not need to act on it: saved, sent, copied, failed. It is transient and it does not block, which also means it is the wrong place for anything they must read.
Deck.toast('Order #1042 shipped');
Deck.toast({
title: 'Project archived',
text: 'You can undo this.',
kind: 'warn',
duration: 8000,
actions: [{ label: 'Undo', onClick: () => restore() }],
});
A string is shorthand for { title }. The defaults are
duration: 5000, dismissible: true, no kind and no actions.
Anatomy
The generated markup, so you can recognise the parts and restyle them. This is rendered here as static markup rather than through the API, so that it holds still to be looked at.
<div class="toast toast-good" role="status" style="max-inline-size:23rem">
<svg class="icon toast-icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check-circle"></use></svg>
<div class="toast-main">
<div class="toast-title">Export finished</div>
<div class="toast-text">2,481 rows written to invoices-2026-03.csv.</div>
<div class="toast-actions">
<button class="toast-action">Download</button>
</div>
</div>
<button class="toast-close" aria-label="Dismiss">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#x"></use></svg>
</button>
</div>
<div class="toast toast-good" role="status" style="max-inline-size:23rem">
<svg class="icon toast-icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check-circle"></use></svg>
<div class="toast-main">
<div class="toast-title">Export finished</div>
<div class="toast-text">2,481 rows written to invoices-2026-03.csv.</div>
<div class="toast-actions">
<button class="toast-action">Download</button>
</div>
</div>
<button class="toast-close" aria-label="Dismiss">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#x"></use></svg>
</button>
</div>
The accent colour is one custom property. --toast-accent paints the
three-pixel edge on the starting side, the icon and the timer bar, so a kind class is
a single declaration.
Kinds
<div class="stack stack-2" style="max-inline-size:23rem">
<div class="toast toast-good" role="status"><div class="toast-main"><div class="toast-title">Good</div></div></div>
<div class="toast toast-warn" role="status"><div class="toast-main"><div class="toast-title">Warn</div></div></div>
<div class="toast toast-bad" role="alert"><div class="toast-main"><div class="toast-title">Bad</div></div></div>
<div class="toast toast-info" role="status"><div class="toast-main"><div class="toast-title">Info</div></div></div>
<div class="toast toast-undo" role="status"><div class="toast-main"><div class="toast-title">Undo</div></div>
<div class="toast-actions"><button class="toast-action">Undo</button></div>
</div>
</div>
<div class="stack stack-2" style="max-inline-size:23rem">
<div class="toast toast-good" role="status"><div class="toast-main"><div class="toast-title">Good</div></div></div>
<div class="toast toast-warn" role="status"><div class="toast-main"><div class="toast-title">Warn</div></div></div>
<div class="toast toast-bad" role="alert"><div class="toast-main"><div class="toast-title">Bad</div></div></div>
<div class="toast toast-info" role="status"><div class="toast-main"><div class="toast-title">Info</div></div></div>
<div class="toast toast-undo" role="status"><div class="toast-main"><div class="toast-title">Undo</div></div>
<div class="toast-actions"><button class="toast-action">Undo</button></div>
</div>
</div>
.toast-loading is the sixth: the same brand accent, but
Deck.toast renders a .spinner in place of an icon. Pair it
with duration: 0 so it stays until you replace it.
const t = Deck.toast({ kind: 'loading', title: 'Submitting…', duration: 0 });
await save();
t.update({ kind: 'good', title: 'Submitted', duration: 4000 });
.toast-undo is a presentational variant rather than a kind: a neutral
accent with the action in the full text colour, for the "archived — undo?" pattern
where the action matters more than the status.
The stack
.toast-region-stacked collapses older toasts behind the newest one,
scaled down and offset, so five notifications do not become five cards down the side
of the screen. Hovering or focusing the region fans them back out.
The index each toast needs for its offset comes from
sibling-index() where the browser has it, with an
:nth-child ladder as the fallback — which is why the source has ten
nearly identical lines after the modern one.
The whole region is pointer-events: none and each toast turns it back on
with pointer-events: auto. Without that, the invisible region would sit
over the bottom corner of the page and swallow clicks on whatever is underneath it.
Timers that pause
.toast-timer is a two-pixel bar that scales from full to zero over
--toast-duration, set from the duration option. It is a CSS
animation rather than a JavaScript countdown, which buys one thing that matters:
.toast-region:is(:hover, :focus-within) .toast-timer { animation-play-state: paused; }
Hovering the stack — or tabbing into it — pauses every timer in it. A reader reaching for the Undo button does not have it disappear as they arrive. That is one declaration, and it would be a scheduler with pause and resume in JavaScript.
Where the region sits
Bottom end by default, which on a phone is where the thumb is. Four modifiers move it,
and .toast-region-top also reverses the flex direction so the stack still
grows away from its edge.
| Class | Position |
|---|---|
.toast-region | Bottom, inline end |
.toast-region-start | Bottom, inline start |
.toast-region-center | Bottom, centred |
.toast-region-top | Top, growing downward |
.toast-region-stacked | Any of the above, collapsed into a stack |
Two details handled for you: env(safe-area-inset-bottom) keeps the stack
clear of a phone's home indicator, and
body:has(.tabbar) .toast-region lifts it by
44px when the page has a bottom tab bar —
so a toast never covers the navigation. Above 48rem, where the tab bar
is not shown, that offset is removed again.
is-leaving and is-dragging
Both are set by deck.js at runtime and are internal —
style them if you like, but do not write them. .is-leaving plays the exit
animation and collapses the toast's margin so the stack closes up;
.is-dragging disables the transition while a swipe follows the pointer,
so the toast tracks the finger exactly rather than lagging behind it.
touch-action: pan-y on the toast is what lets a horizontal swipe be
captured for dismissal while a vertical drag still scrolls the page.
Classes
Generated from src/13-toasts.css by tools/docs/extract.mjs.
| Class | Layer | What it does | Source |
|---|---|---|---|
.toast |
deck.components |
One notification. The accent colour is a single custom property, --toast-accent, which paints the edge, the icon and the timer together, so a kind is one declaration. Built by Deck.toast() rather than written by hand; touch-action: pan-y lets a horizontal swipe dismiss it while a vertical drag still scrolls the page. | 13-toasts.css:49 |
.toast-action |
deck.components |
Sets font-size, font-weight, color, cursor, and 1 more. | 13-toasts.css:102 |
.toast-actions |
deck.components |
Sets display, gap, margin-block-start. | 13-toasts.css:101 |
.toast-bad |
deck.components |
Sets --toast-accent. | 13-toasts.css:114 |
.toast-close |
deck.components |
Sets flex, inline-size, block-size, display, and 6 more. | 13-toasts.css:88 |
.toast-good |
deck.components |
Sets --toast-accent. | 13-toasts.css:112 |
.toast-icon |
deck.components |
The leading glyph, coloured by --toast-accent so it matches the kind without a rule of its own. | 13-toasts.css:82 |
.toast-info |
deck.components |
Sets --toast-accent. | 13-toasts.css:115 |
.toast-loading |
deck.components |
Sets --toast-accent. | 13-toasts.css:116 |
.toast-main |
deck.components |
The text column of a toast. min-inline-size: 0 so a long unbroken string cannot push the close button off the end. | 13-toasts.css:85 |
.toast-region |
deck.components |
The fixed container. pointer-events: none on the region and auto on each toast, so the empty area does not swallow clicks on the page beneath. Sits above a .tabbar when the page has one, and clears the home indicator. | 13-toasts.css:12 |
.toast-region-center |
deck.components |
Sets inset-inline, margin-inline. | 13-toasts.css:32 |
.toast-region-stacked |
deck.components |
No declarations. | 13-toasts.css:161 |
.toast-region-start |
deck.components |
Sets inset-inline. | 13-toasts.css:31 |
.toast-region-top |
deck.components |
Sets inset-block, flex-direction. | 13-toasts.css:33 |
.toast-text |
deck.components |
Sets color. | 13-toasts.css:87 |
.toast-timer |
deck.components |
The auto-dismiss bar. A CSS animation rather than a JavaScript countdown, which is what allows hovering or focusing the region to pause every timer in it with one declaration — so a reader moving toward an Undo button does not have it vanish as they arrive. | 13-toasts.css:123 |
.toast-title |
deck.components |
Sets font-weight. | 13-toasts.css:86 |
.toast-undo |
deck.components |
Sets --toast-accent. | 13-toasts.css:194 |
.toast-warn |
deck.components |
Sets --toast-accent. | 13-toasts.css:113 |
.is-dragging |
deck.components |
No declarations. | 13-toasts.css:191 |
.is-leaving |
deck.components |
No declarations. | 13-toasts.css:148 |
Tokens
| Token | Live | Declared value |
|---|---|---|
--surface |
light-dark(var(--ink-0), var(--ink-950)) |
|
--surface-hover |
light-dark(var(--ink-50), var(--ink-900)) |
|
--line |
light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral))) |
|
--text |
light-dark(var(--ink-900), var(--ink-100)) |
|
--text-muted |
light-dark(var(--ink-600), var(--ink-400)) |
|
--text-faint |
light-dark(var(--ink-500), var(--ink-500)) |
|
--text-sm |
.8125rem |
|
--brand |
light-dark(var(--brand-600), var(--brand-400)) |
|
--brand-500 |
oklch(60.8% var(--chroma-brand) var(--hue-brand)) |
|
--good-500 |
oklch(62% var(--chroma-status) var(--hue-good)) |
|
--warn-500 |
oklch(76% var(--chroma-status) var(--hue-warn)) |
|
--bad-500 |
oklch(58% calc(var(--chroma-status) * 1.25) var(--hue-bad)) |
|
--ink-400 |
oklch(70.8% calc(var(--chroma-neutral) * 2.6) var(--hue-neutral)) |
|
--ink-500 |
oklch(58.4% calc(var(--chroma-neutral) * 2.8) var(--hue-neutral)) |
|
--r-md |
12px |
|
--r-full |
999px |
|
--shadow-4 |
0 8px 16px oklch(20% .03 var(--shadow-color) / .08), 0 24px 56px oklch(20% .03 var(--shadow-color) / .14) |
|
--tap |
44px |
|
--space-1 |
.25rem |
|
--space-2 |
.5rem |
|
--space-3 |
.75rem |
|
--space-4 |
1rem |
|
--space-6 |
1.5rem |
|
--z-toast |
— | 500 |
--dur-2 |
190ms |
|
--dur-3 |
300ms |
Accessibility
-
The region is a labelled landmark.
Deck.toastsetsrole="region"andaria-label="Notifications"on it when it mounts, so a screen-reader user can navigate to the stack deliberately. -
The role depends on the kind.
kind: 'bad'getsrole="alert", which interrupts; everything else getsrole="status", which waits for a pause. That is the right split — a failure should interrupt, "Copied" should not. -
Auto-dismiss is a genuine accessibility problem. WCAG asks that
content which disappears on a timer can be paused, stopped or extended. Deck's
pause-on-hover-or-focus satisfies that for a reader who can reach the stack — but a
screen-reader user who has not focused it still has five seconds. For
anything with an action in it, pass
duration: 0and let the reader dismiss it. -
Toasts are far from the focus point. A message in the bottom corner
is easy to miss if the reader is looking at the top of a form. For a result they must
see, put an
.alertin the page as well. - Swipe-to-dismiss is pointer-only, but the close button and the timer both work without it, so nothing is unreachable — unlike drag-to-reorder, where the gesture is the only path.
-
The close button has a label.
aria-label="Dismiss", set by the generator. At 26px it is under the touch minimum, which is the fourth such place in Deck — mitigated here by the whole toast being swipeable. - The queue caps at eight. Older toasts are dismissed automatically past that, so a burst of activity cannot fill the screen or the accessibility tree.
-
Text is escaped.
Deck.toastrunsescapeHTMLover the title and text, so a message built from user input cannot inject markup.
Right to left
The region's placement uses inset-inline, the accent edge is
inset-inline-start, and the close button's negative offset is
margin-inline-end — so the default bottom-end region moves to the bottom
left under dir="rtl" and the accent stripe moves to the right side of
each toast. All of that is free.
One thing is not. The timer bar shrinks with
transform-origin: left center, and transform-origin takes
physical keywords with no logical equivalent — so a bar that should drain toward the
starting edge would drain the wrong way. src/19-logical.css corrects it,
alongside the chart bars and progress fills that have the same problem:
[dir="rtl"] :is(.chart-bar-fill, .progress::-webkit-progress-value, .toast-timer) {
transform-origin: right center;
}
This is the third property in Deck with no logical form, after
background-position and the
drawer's translate. They are all collected in one file
rather than scattered as overrides beside each component.
<div dir="rtl" class="toast toast-good" role="status" style="max-inline-size:23rem">
<svg class="icon toast-icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check-circle"></use></svg>
<div class="toast-main">
<div class="toast-title">اكتمل التصدير</div>
<div class="toast-text">تمت كتابة ٢٤٨١ صفًا.</div>
</div>
<button class="toast-close" aria-label="إغلاق">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#x"></use></svg>
</button>
</div>
<div dir="rtl" class="toast toast-good" role="status" style="max-inline-size:23rem">
<svg class="icon toast-icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check-circle"></use></svg>
<div class="toast-main">
<div class="toast-title">اكتمل التصدير</div>
<div class="toast-text">تمت كتابة ٢٤٨١ صفًا.</div>
</div>
<button class="toast-close" aria-label="إغلاق">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#x"></use></svg>
</button>
</div>
Reduced motion
Toasts animate in, animate out, and move as the stack reflows. Under
prefers-reduced-motion: reduce the global reset collapses every
transition to .01ms and every animation to a single iteration, so they
appear and disappear in place.
The timer bar is the interesting case. It is an animation rather than a
transition, and the reset sets animation-duration: .01ms — which would
make the bar vanish immediately. It still works because the bar is decoration: the
dismissal is a JavaScript timer, not the animation's end. A reader with reduced motion
loses the visual countdown and keeps the behaviour, which is the right way round.
Printing
src/99-print.css lists .toast-region among the chrome that
never prints, with the tab bar and the skip link. A toast reports something transient;
printing one would be printing a moment.
Overriding it
/* Choose the placement once, before the first toast */
Deck.toasts.stacked = true;
@layer app.components {
/* A wider stack on desktop */
@media (min-width: 64rem) {
.toast-region { inline-size: min(28rem, calc(100vw - var(--space-6))); }
}
/* A kind of your own — one declaration */
.toast-brand { --toast-accent: var(--brand-600); }
}
A custom kind needs the class on the toast, which means passing
kind: 'brand' — the generator builds the class name as
toast- plus the kind, so any suffix works as long as you have styled it.
When not to use it
-
Not for anything the reader must act on. It disappears. Use an
.alertin the page, or a.modalif it blocks. -
Not for form validation. The error belongs beside the field, in an
.errorwitharia-describedby. A toast tells the reader something is wrong and then takes away the only description of it. - Not for a question. "Are you sure?" needs an answer, and a toast can be missed entirely. That is a modal.
-
Not for progress that lasts. A loading toast is right for a few
seconds. For a job that runs for minutes, put a
.progressin the page where the reader can find it again. - Not several at once, deliberately. The stack handles a burst gracefully, but three toasts for one action is three interruptions. Summarise instead.
-
Not with an action and a short duration. If there is an Undo
button, use
duration: 0. A five-second window to notice, read and reach a button is not a window at all for many readers.