Susegad UI
Register
Theme
Palette

Components

Toast

Short messages that never take focus. A region holds two live regions (polite, and alert for errors) and a pile of letters: the newest in front, the rest peeking above, fanning out when pointed at or focused.

quiet warm playful

Open the live demo npx susegad add toast

Stands on: Core, Core: components, Sound, Tokens

The prompt

the prompt

A short message that arrives without taking focus: Saved, Couldn't save, The photo was deleted (Undo).

Screen readers hear every toast as it arrives. Sighted readers see a small pile of letters in the corner. Nobody loses a message to a timer they could not stop.

<link rel="stylesheet" href="susegad/components/toast/toast.css">
<script type="module" src="susegad/components/toast/toast.js"></script>

<sg-toast-region></sg-toast-region>

<script type="module">
  import { toast } from './susegad/components/toast/toast.js';
  toast('Your changes are saved.', { tone: 'success', title: 'Saved' });
</script>

<!-- or from a server, e.g. an htmx swap into the region -->
<sg-toast tone="error"><strong class="sg-toast-title">Couldn't save</strong> Check your connection and try again.</sg-toast>
AttributeOnValuesWhat it does
tonesg-toastinfo, success, warning, errorThe glyph on the stamp, the edge colour and the live region. Errors are spoken as alerts and never time out.
durationsg-toastmsAsks for a time. Never less than 5 s; ignored for errors and toasts with an action.
durationsg-toast-regionms, or 00 turns every timeout off (a setting to offer people who need it); a number is the least time any toast gets.
maxsg-toast-region1 or more, default 3How many letters are in the pile; the rest wait, untimed.
layoutsg-toast-regionpile, listpile shows the newest in front; list lays them all out.
positionsg-toast-regionbottom-end, bottom-center, top-end, top-centerWhere the pile sits.
hotkeysg-toast-regione.g. Alt+Shift+T (the default), F8, or noneMoves focus to the newest toast, only when pressed.

The prompt

Make a toast notification web component in two parts: a region placed once on the page, <sg-toast-region>, and the toasts, <sg-toast tone="success">, which a script or a server can add. Never move focus to a toast. Put two visually hidden live regions in the region before any message arrives, one role="status" with aria-live="polite" and one role="alert" for errors, and append each toast's words to the right one as it arrives, with "Error:" or "Warning:" in front so the tone is heard, not only seen. Give every toast a tone glyph (a circled i, a tick, a triangle, an octagon with a cross) so colour never carries the meaning alone, and a Dismiss button named after the message. Follow WCAG 2.2.1: keep each toast for the time it takes to read it slowly (two seconds to notice plus a third of a second a word, never less than five seconds), never time out an error or a toast with an action, let the page turn timeouts off, and stop the clock while the pointer is over the toasts, while focus is inside them and while the tab is hidden. Stack them as a pile of letters, the newest in front and the older ones peeking above; spend time only on the one in front, and fan them out into a list when pointed at or focused. Show at most three and keep the rest waiting. Let Alt+Shift+T (not Alt+T, which opens Firefox's Tools menu) move focus to the newest toast on request, Escape dismiss the focused one and move to the next, and focus return to where the person was after the last. Use the popover API so the pile sits above everything, and never let it hide the focused control: publish the pile's height so the page scrolls focus clear of it, leave room at the end of the page, and fold the pile to its front letter if focus lands under it anyway. Give it three registers. Quiet: a plain ruled note with a coloured edge that fades in and out in under 200 ms. Warm: an India Post inland letter, blue, with a dashed inner rule, creases at the top corners and the tone glyph printed on a perforated stamp; the flap swings open on its top edge as the card opens downward. Playful: the same letter, and a postmark lands on the stamp with a small press: two rings, words round the ring, the date and time in the middle, wavy cancellation lines and a rough ink edge. Under reduced motion, show the letter open and the postmark in place, with nothing moving.

Words to code

When you sayTechniqueWhat happens
never move focus to a toastnative firstNothing calls focus() on a toast unless the person pressed the hotkey. A browser check types into a field while two toasts arrive and confirms focus stays there.
two visually hidden live regions … before any message arriveslive regionsThe region builds role="status" and role="alert" containers when it connects, so screen readers are already listening. Each message is appended as a new line 60 ms later (a timer, because frames stop in a hidden tab) and removed after 15 s.
"Error:" or "Warning:" in frontredundancyannouncement() prefixes the tone in words, and a visually hidden span does the same inside the toast for anyone who reads it later.
the time it takes to read it slowlyreading timereadingTime() is 2 s plus 333 ms a word (180 words a minute), rounded up to 100 ms and held between 5 s and 20 s. Unit-tested.
never time out an error or a toast with an actionWCAG 2.2.1durationFor() returns 0 (until dismissed) for errors and for any toast that holds a button or link, and for every toast when the region's duration is 0.
stop the clock while …pauseThe region keeps a set of reasons (pointer, focus, hidden tab, hold()). Before any change it settles the clock, so paused time and time before a toast arrived are never charged to it. A browser check hovers for 7 s and the toast is still there.
focus is never hidden under the pileWCAG 2.4.11A ResizeObserver on the pile (and transitionend, since the peeking letters move by transform) measures how far the letters reach up the viewport and sets --sg-toast-pile on :root. toast.css makes that scroll-padding-block-end on html and a spacer after body. On a focusin under the pile, it folds to its front letter and scrolls the control into view. A browser check tabs every control at 390 px with an error showing and requires 0% covered.
a pile of letters … spend time only on the one in frontqueuecreateQueue({ front: true }) shows the first max toasts and ticks only the newest of them. The ones behind keep their full time until they come forward, and the ones waiting spend nothing. Unit-tested.
fan them out into a list when pointed at or focusedCSS gridPiled, every letter sits in one grid cell, pushed up by --depth × 0.55 rem and scaled down slightly. The letters behind are cut to the front letter's height (a ResizeObserver) and their words are hidden, so nothing unreadable can take focus.
Alt+Shift+T … Escape … focus returnkeyboardparseHotkey() matches on the physical key (KeyT), so Option+Shift+T on a Mac works. The default adds Shift because Alt+T alone opens Firefox's Tools menu. Escape dismisses the focused toast and focus moves to the next, then back to the element that had it before.
the popover API so the pile sits above everythingnative firstThe stack is popover="manual", shown while it holds a toast, so it is in the top layer without z-index battles. Without popover support it is position: fixed.
an India Post inland lettertexturelight-dark(var(--sg-inland), …) for the blue, an outline with a negative offset for the dashed rule, and two diagonal gradients for the corner creases. On the blue the action is set in ink with an accent underline, because the accent drops below 4.5:1 there in some palettes.
a perforated stampCSS maskTwo mask layers: a grid of small transparent circles, and a solid rectangle 6 px smaller. Together they cut the holes only along the edge.
the flap swings open … as the card opens downwardWAAPIThe flap is a triangle (clip-path) turned from 0 to 180 degrees about its top edge with perspective, while the card's clip-path: inset() opens from 58% to 0. The animations are cancelled when done, so getAnimations() stays empty.
a postmark lands … with a small presseasingKeyframes from scale 1.7 and no ink to 0.9, then settling at 1 with a slight turn, on an overshooting curve, after the letter has opened. The ring words follow a circle with textPath, and feTurbulence into feDisplacementMap roughens the ink.
under reduced motion … nothing movingstillEvery skin checks ctx.motion === 'still' and shows the finished letter; a browser check confirms no animations run.

<sg-toast-region> and <sg-toast> show short messages that never take focus. Every toast is spoken as it arrives. The visible toasts form a small pile of letters: the newest in front, the older ones peeking above it.

Use

<link rel="stylesheet" href="susegad/components/toast/toast.css">
<script type="module" src="susegad/components/toast/toast.js"></script>

<sg-toast-region></sg-toast-region>

From a script:

import { toast } from './susegad/components/toast/toast.js';

toast('Your changes are saved.', { tone: 'success', title: 'Saved' });
toast('The photo was deleted.', { action: { label: 'Undo', onAction: restore } });

const region = document.querySelector('sg-toast-region');
region.show({ tone: 'error', title: "Couldn't save", message: 'Check your connection and try again.' });

From a server: append an <sg-toast> to the region (for example hx-swap="beforeend" targeting it). The region moves it into the pile and announces it.

<sg-toast tone="error"><strong class="sg-toast-title">Couldn't save</strong> Check your connection and try again.</sg-toast>

An <sg-toast> outside a region is shown where it stands, as an inline note with a Dismiss button. Without JavaScript it is a plain ruled note in the flow of the page.

Attributes

ElementAttributeValuesDefault
sg-toasttoneinfo, success, warning, errorinfo
sg-toastdurationms, at least 5000; 0 stays until dismissedthe reading time
sg-toast-regionduration0 turns every timeout off; a number is the least any toast getsper toast
sg-toast-regionmaxhow many are in the pile3
sg-toast-regionlayoutpile, listpile
sg-toast-regionpositionbottom-end, bottom-center, top-end, top-centerbottom-end
sg-toast-regionhotkeya key such as Alt+Shift+T or F8; noneAlt+Shift+T
sg-toast-regionlabelthe name of the notifications regionNotifications
bothregisterquiet, warm, playfulinherited

Methods and events

region.show(options | element){ message, title, tone, duration, action: { label, onAction, keep } }, or an <sg-toast> you made. Returns the toast. An action dismisses its toast after it runs, unless keep is set (or the button has data-keep).
region.hold(on = true)Stops (or restarts) every clock, for a "hold messages" setting.
toast.dismiss(reason)Plays the skin's exit and removes the toast.
toast(message, options)Shows a toast in the page's region, making one at the end of <body> if there is none.
sg-toast-dismissBubbles from the toast: { reason: 'user' | 'timeout' | 'action' | 'api', id }.

A toast plays confirm on arrival, or error for an error-toned one, through the sound switch (packages/sound). Silent unless the switch is on and a gesture has already happened; nothing to wire up.

Timing (WCAG 2.2.1)

  • A toast stays for its reading time: 2 s plus 333 ms a word, at least 5 s and at most 20 s.
  • Errors, and toasts with a button or link, stay until they are dismissed.
  • The clock stops while the pointer is over the pile, while focus is inside it, and while the tab is hidden.
  • In the pile only the front letter spends time. The ones behind keep their full time until they come forward; those waiting beyond max spend nothing.
  • duration="0" on the region turns timeouts off. Offer it as a setting to people who need more time.

Registers

LookMotion
quieta plain ruled note, a coloured edge, the tone glyphfades in and out in under 200 ms
warman inland letter: blue card, dashed rule, corner creases, the glyph on a perforated stampthe flap swings open as the card opens downward
playfulthe letter, and a postmark on the stamp with the ring words, date and timethe postmark lands with a small press

Under reduced motion every register shows the finished letter at once.

Accessibility

  • Focus is never moved to a toast. Alt+Shift+T (the region's hotkey; plain Alt+T would open Firefox's Tools menu on Windows and Linux) moves to the newest when the person asks; Escape dismisses the focused toast, focus moves to the next, and after the last it returns to where it was.
  • Two live regions exist before any message: polite for most, role="alert" for errors. The tone is spoken ("Error: …") and drawn as a distinct glyph, never colour alone.
  • Every Dismiss button is named after its message ("Dismiss: Saved …").
  • Focus is never hidden under the pile (WCAG 2.4.11). The region publishes how far the pile reaches up the viewport as --sg-toast-pile on :root; toast.css turns it into scroll-padding-block-end on html, so browsers scroll focus clear of it, and a spacer at the end of body (body::after, so your own body padding is left alone) lets the last controls scroll clear. If focus still lands under the pile, it folds to its front letter and the control is brought into view. A top pile sets scroll-padding-top instead.
  • The pile is a named region ("Notifications"), so screen reader users can jump to it. Letters behind the front one hide their words and cannot take focus until the pile fans out.
  • Contrast: message text is 11:1 or more on the letter in every palette and theme. On the letter, actions are set in ink with an accent underline, because the accent is under 4.5:1 on the blue in Susegad dark and Casa light.
  • Keep messages short and specific. Errors say what happened and how to fix it.

Checks

node --test packages/components/toast/toast.test.js covers the pure core: reading time, durations, the pile queue, announcements, the hotkey and the postmark. node packages/components/toast/toast.check.mjs checks focus, the live regions, pausing, the timeout, the keyboard and reduced motion in Chromium.