Skip to content

Toasts

2 min read · Updated 8 Oct 2026

A toast reports that a background action finished or failed. It appears at the window’s edge, does not block the page and does not take focus. Also show every error where it happened, for example in the object’s state.

Toasts: A brief notice confirms a background action at the edge of the page.

Illustrative example with synthetic data. A brief notice confirms a background action at the edge of the page.

Use one to confirm an action whose result is not otherwise visible, or to summarise failures across the page in one notice.

The page needs Use
A decision before the reader goes on A confirmation dialog
The state of one object Object states
A failed start-up The boot loader
A result of an action in the data dialog The dialog itself; see Open the dialog immediately
  1. Did something the reader asked for fail, or does something need their attention? Use an error.
  2. Otherwise, use a notice.
Kind Region Dismissal Common use cases
Notice role="status" After 5 seconds; paused on hover and focus “Revenue by region.xlsx downloaded.” “Value copied.”
Error role="alert" Only by the reader “The image could not be exported.” “3 objects could not be refreshed.”
<!-- At the end of <body>, outside every dialog -->
<section class="toasts" aria-label="Notifications">
<div class="toasts__list" role="status"></div>
<div class="toasts__list" role="alert"></div>
</section>
  • Create both regions at page load. A live region created together with its content is often not announced.
  • Name the object in the text (“Revenue by region”), so several toasts can be told apart.
:root {
--toast-w: 360px;
/* Set to the height of anything fixed along the bottom edge. */
--toast-bottom: 0px;
}
.toasts {
position: fixed;
inset: auto var(--gap) calc(var(--gap) + var(--toast-bottom)) auto;
z-index: 10;
display: grid;
gap: 8px;
width: min(var(--toast-w), 100% - 2 * var(--gap));
/* The region takes no clicks; only toasts do. */
pointer-events: none;
}
.toasts__list {
display: grid;
gap: 8px;
}
.toast {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
padding: 12px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
color: var(--ink);
box-shadow: var(--panel-shadow);
pointer-events: auto;
}
.toasts__list[role='alert'] > .toast {
border-inline-start: 4px solid var(--danger);
}
.toast__text {
margin: 0;
overflow-wrap: anywhere;
}
.toast__close {
width: 24px;
height: 24px;
}
@media (prefers-reduced-motion: no-preference) {
.toast {
animation: toast-in 150ms ease-out;
}
}
@keyframes toast-in {
from {
opacity: 0;
translate: 0 8px;
}
}
@media (width < 600px) {
.toasts {
inset-inline: var(--gap);
width: auto;
}
}

Place toasts at the bottom, clear of the sticky header and filter bar. The entry animation uses translate, so it does not replace a placement transform.

type Kind = 'notice' | 'error';
const NOTICE_MS = 5000;
const MAX_NOTICES = 3;
const lists = {
notice: document.querySelector<HTMLElement>('.toasts__list[role="status"]'),
error: document.querySelector<HTMLElement>('.toasts__list[role="alert"]'),
};
// Move focus out before removing a focused toast, so it is not lost. #main needs tabindex="-1".
const removeToast = (item: HTMLElement) => {
if (item.contains(document.activeElement)) document.querySelector<HTMLElement>('#main')?.focus();
item.remove();
};
export const toast = (text: string, kind: Kind = 'notice') => {
const list = lists[kind];
if (!list) return;
const item = document.createElement('div');
item.className = 'toast';
const body = document.createElement('p');
body.className = 'toast__text';
body.textContent = text;
const close = document.createElement('button');
close.type = 'button';
close.className = 'toast__close';
close.setAttribute('aria-label', 'Dismiss');
close.textContent = '×';
item.append(body, close);
let timer: ReturnType<typeof setTimeout> | undefined;
const dismiss = () => {
clearTimeout(timer);
removeToast(item);
};
close.addEventListener('click', dismiss);
if (kind === 'notice') {
const start = () => {
clearTimeout(timer);
if (item.matches(':hover') || item.contains(document.activeElement)) return;
timer = setTimeout(dismiss, NOTICE_MS);
};
item.addEventListener('pointerenter', () => clearTimeout(timer));
item.addEventListener('pointerleave', start);
item.addEventListener('focusin', () => clearTimeout(timer));
item.addEventListener('focusout', (event) => {
if (!(event.relatedTarget instanceof Node) || !item.contains(event.relatedTarget)) start();
});
start();
while (list.children.length >= MAX_NOTICES) {
const oldest = list.firstElementChild;
if (!(oldest instanceof HTMLElement)) break;
removeToast(oldest);
}
}
list.append(item);
};
  • Errors stay until dismissed.
  • A toast never takes focus; its close button is reachable with Tab.
  • Put no action in a notice, because it disappears on its own. Put actions in an error or in the page.
Event Kind Text
CSV or Excel file created Notice “Revenue by region.csv downloaded.”
Image export failed Error “The image of Revenue by region could not be exported.”
Value copied Notice “Copied 1,234.5.”
Copy failed Error “Copy failed: the browser blocked clipboard access.”
Objects failed after an applied change Error “3 objects could not be refreshed.”
  • Summarise failures that happen together in one toast.
  • Do not announce what the page already shows, such as applied selections.
  • Format numbers with the app’s locale; see Numbers: pin one locale.
  • navigator.clipboard.writeText rejects when the browser refuses; report both outcomes.

A modal dialog makes the page, including the toast region, inert. Report results of actions taken in a dialog inside that dialog.

Width Region
600px and wider Bottom-right, 360px wide
Under 600px Along the bottom, full width less the gaps