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.

Icon, main, actions, close — and a timer bar when duration is not 0
Export finished
2,481 rows written to invoices-2026-03.csv.
<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

Five accents. Note the role changes with the kind.
Good
Warn
Info
Undo
<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.

Toast region placements
ClassPosition
.toast-regionBottom, inline end
.toast-region-startBottom, inline start
.toast-region-centerBottom, centred
.toast-region-topTop, growing downward
.toast-region-stackedAny 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.

Classes, generated from the stylesheet source
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

Design tokens this component reads
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.toast sets role="region" and aria-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' gets role="alert", which interrupts; everything else gets role="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: 0 and 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 .alert in 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.toast runs escapeHTML over 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.

The accent edge follows the writing direction
اكتمل التصدير
تمت كتابة ٢٤٨١ صفًا.
<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 .alert in the page, or a .modal if it blocks.
  • Not for form validation. The error belongs beside the field, in an .error with aria-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 .progress in 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.