Component
toast
Demo
tokens.css,
base.css and toast.css. No scripts.
Source
Copy both files, or run npx nojsui add toast.
Browser support
Shipped in every major engine, but only recently — older versions need the fallback.
| Feature | Baseline | Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari iOS |
|---|---|---|---|---|---|---|---|---|
| Popover | newly | 116 | 116 | 125 | 17 | 116 | 125 | 18.3 |
| Invoker commands | newly | 135 | 135 | 144 | 26.2 | 135 | 144 | 26.2 |
Usage
<button type="button" command="show-popover" commandfor="saved">Save</button>
<div id="saved" class="sk-toast" popover="manual" role="status" data-sk-motion="essential">
<p class="sk-toast__text">Saved to your library</p>
<button type="button" class="sk-toast__close" command="hide-popover" commandfor="saved" aria-label="Dismiss">
<span aria-hidden="true">×</span>
</button>
</div>
Set how long it stays with --sk-toast-life (default 5s) — see Theming below.
What this component is, and is not
A toast is triggered by something happening — a save completing, a request failing. CSS cannot observe that, so this component does not pretend to be a notification system. What it is:
- the region, its placement and its styling
- a CSS-only auto-dismiss that removes the message from the screen and from the accessibility tree on a timer
- a manual dismiss that needs no script
In a real application the trigger is your own code calling showPopover(), or
an invoker button as in the demo. Everything after the trigger is script-free.
Use alert instead when the message must stay — a
form validation error, a success panel that replaces a form, anything the user
has to act on. It renders in normal flow, has no timer, and can be shown as
many times as you like.
The auto-dismiss
An animation ends on visibility: hidden and animation-fill-mode: forwards
holds it there. visibility is a discrete property, so it flips at the end of
the timeline rather than fading — which is exactly what makes the message
leave rather than linger invisibly.
Verified in Chromium, WebKit and Gecko: while the toast is up, the region
appears in the accessibility tree as status: Saved to your library; after the
timeline finishes, it is gone from the tree entirely. Not transparent-but-present
— absent.
Why this component opts out of the reduced-motion clamp
data-sk-motion="essential" exempts the region from base.css’s global
reduced-motion clamp. This is the only component in the kit that does it, and
ADR 0008 is the
justification.
Without the exemption, prefers-reduced-motion: reduce compresses the
auto-dismiss timeline to 0.01ms. Measured before the fix: the toast was
visibility: hidden 120ms after being shown — the message was never readable at
all. The floor meant to protect the user from motion was destroying the
component’s function instead. WCAG 2.3.3 exempts motion essential to
functionality for the same reason.
Reduced motion still changes the experience. Under reduce the toast drops
its slide-in and fades only. The timing is identical, so the message is
readable for exactly as long either way — which is the point.
Known limitation: showing it twice
CSS can end the animation, but it cannot close a popover. After an auto-dismiss
the region is invisible and out of the accessibility tree, but still technically
open — so a second command="show-popover" does nothing until something hides
it first.
The dismiss button (command="hide-popover") resets it, which is why the demo
has a Reset button next to Save. In an application this rarely comes up: whatever
triggers your toasts will hide the region before showing it again.
Theming
Set any of these anywhere above the component — :root, a section wrapper, or
one instance. toast.css only ever reads them, so the nearest declaration wins
(ADR 0011).
| Property | Default | Controls |
|---|---|---|
--sk-toast-life | 5s | How long the message stays before it removes itself |
--sk-toast-bg | --sk-color-surface-raised | Message background |
--sk-toast-fg | --sk-color-text | Message text |
--sk-toast-border-color | --sk-color-border | Message border |
--sk-toast-radius | --sk-radius-lg | Message corner radius |
--sk-toast-shadow | --sk-shadow-lg | Message elevation |
--sk-toast-inline-size | 24rem | Message width before the viewport clamp |
--sk-toast-close-fg | --sk-color-text-muted | Close button glyph |
--sk-toast-close-bg-hover | --sk-color-surface | Close button background on hover |
--sk-toast-button-bg | --sk-color-surface | Trigger background |
--sk-toast-button-bg-hover | --sk-color-surface-raised | Trigger background on hover |
--sk-toast-button-fg | --sk-color-text | Trigger text |
--sk-toast-button-border-color | --sk-color-border-strong | Trigger border |
--sk-toast-button-radius | --sk-radius-md | Trigger corner radius |
:root { --sk-toast-life: 8s; }
--sk-toast-life is the one knob here that changes behaviour rather than
appearance, and it is deliberately exposed: how long a message is readable is a
content decision, not ours. Give people enough time to read it — WCAG 2.2 SC
2.2.1 applies, and the close button is what makes the timing adjustable at all.
Keyboard contract
| Key | Behaviour |
|---|---|
Tab | Reaches the dismiss button while the toast is up |
Enter / Space on dismiss | Hides it immediately |
The toast never takes focus — verified in all three engines. A message that stole focus would interrupt whatever the user was doing, which is precisely what a toast is supposed to avoid.
Verified manually in VoiceOver and NVDA: not yet — do this before the
component is marked done. role="status" announcement timing is the thing to
check: it is polite, so it should be read at the next opportunity rather than
interrupting.
Accessibility notes
role="status"makes the region a polite live region: announced without interrupting. Do not change it torole="alert"unless the message is genuinely urgent —alertinterrupts.popover="manual", notauto. Light dismiss would let an unrelated click swallow a message the user has not read.- The dismiss button has a real label.
×is decorative andaria-hidden; the button carriesaria-label="Dismiss". - Do not put essential information only in a toast. It removes itself on a timer by design. Anything the user may need again belongs somewhere permanent.
Degradation
Baseline column from support.json — regenerate with pnpm support.
| Feature used | Baseline status | Behavior without it |
|---|---|---|
| Popover API | newly | The region renders inline in normal flow instead of the top layer, as a plain block rather than the flex row. It still announces and still auto-dismisses; it just is not pinned to the corner. |
| Invoker Commands | newly | The demo’s buttons do nothing. An application triggering the toast from its own code is unaffected — see docs/polyfills.md for the opt-in shim. |
The auto-dismiss itself needs only CSS animations and visibility, which are
Baseline widely available — so the part that is hardest to do without script is
also the part with the fewest support caveats.