Primitives Overlays
Toast
Short feedback that appears in a corner and goes away by itself. The region is a manual popover in the top layer and its list a polite live region; each toast is created by showToast() and carries one timer. Tier 2.
- Tier
- 2 · Lazy scoped controller
- Behavior JS
- 1396 B brotli · 1547 B gzip ·
packages/· methodprimitives/ artifacts/ size.json - Platform features
popover="manual",aria-live="polite",showPopover(),:popover-open- Shared listeners
- click, pointerover, pointerout, focusin, focusout
- Per-instance listeners
- scoped to one interaction session
- Lazy state
- WeakMap entry created on first interaction
- Native base
popover="manual" region + aria-live list
On this page
Example
<section data-slean="toast" popover="manual" aria-label="Notifications">
<ol data-slean-part="list" aria-live="polite"></ol>
</section>
<!-- showToast() adds, for each toast: -->
<li data-slean-part="item" data-variant="success">
<p data-slean-part="title">Invoice sent</p>
<p data-slean-part="description">harbor.studio receives it in a minute.</p>
<button type="button" data-slean-part="action" data-slean-value="undo">Undo</button>
<button type="button" data-slean-part="close" aria-label="Dismiss"></button>
</li><script lang="ts">
// With @svelte-lean/vite the register import is injected for the static data-slean="toast".
import '@svelte-lean/styles/toast.css';
import { showToast, type ToastActionDetail } from '@svelte-lean/primitives/toast';
import { on } from 'svelte/events';
let region: HTMLElement;
function sent() {
showToast(region, {
title: 'Invoice sent',
description: 'harbor.studio receives it in a minute.',
tone: 'success',
action: { label: 'Undo', value: 'undo' }
});
}
</script>
<button type="button" onclick={sent}>Send invoice</button>
<section
bind:this={region}
data-slean="toast"
popover="manual"
aria-label="Notifications"
{@attach (node) =>
on(node, 'slean:action', (event) => {
console.log((event as CustomEvent<ToastActionDetail>).detail.value);
})}
>
<ol data-slean-part="list" aria-live="polite"></ol>
</section>Why this implementation exists
A toast has to render above everything, including an open dialog, without being closed by a click elsewhere, and a screen reader has to hear it without focus moving. popover="manual" gives the first: the top layer, no light dismiss. aria-live="polite" on the list gives the second.
What remains is time: each toast leaves after its duration, and the reader must be able to stop the clock (WCAG 2.2.1). That is ephemeral memory per toast, which is what Tier 2 is for. The timer lives in a WeakMap entry created by showToast() and deleted when the toast goes; pointer or focus in the region pauses every timer and leaving resumes them with the time that was left.
The browser owns
- the top layer, above dialogs, without light dismiss (popover="manual")
- announcing a new toast without moving focus (aria-live="polite")
- the action and close buttons: focus, activation, their names
Svelte Lean owns
- showToast(): the item with its title, description, action and close button
- one timer per toast in a WeakMap, created when shown and gone when dismissed
- pausing every timer while the pointer or focus is inside, resuming with the time left
- the cancelable slean:dismiss with its reason, slean:action; hiding the empty region
- toast.css: six positions, tones from the soft tokens, the entry transition
Usage
Install @svelte-lean/primitives for the behavior and @svelte-lean/styles for the stylesheet. With @svelte-lean/vite the
registration is injected for every static data-slean="toast"; without it, import
the register module once.
npm install @svelte-lean/primitivespnpm add @svelte-lean/primitivesyarn add @svelte-lean/primitivesbun add @svelte-lean/primitivesimport '@svelte-lean/primitives/toast/register';import '@svelte-lean/styles/toast.css';Render one empty region per page, usually in the root layout, and keep a reference to it. Call showToast(region, options) with a title, and optionally a description, a tone (success, warning, danger), a duration in milliseconds (0 keeps it) and one action.
Listen to slean:action on the region for the action button’s value and to slean:dismiss to know why a toast left; cancelling a dismissal keeps the toast.
<script lang="ts">
import type { ToastDismissDetail } from '@svelte-lean/primitives/toast';
import { on } from 'svelte/events';
// Keep an error until it has been read: cancel the timeout, allow the close button.
function dismiss(event: Event) {
const { item, reason } = (event as CustomEvent<ToastDismissDetail>).detail;
if (reason === 'timeout' && item.dataset.variant === 'danger') event.preventDefault();
}
</script>
<section
data-slean="toast"
popover="manual"
aria-label="Notifications"
{@attach (node) => on(node, 'slean:dismiss', dismiss)}
>
<ol data-slean-part="list" aria-live="polite"></ol>
</section>Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <section data-slean="toast" popover="manual" aria-label="Notifications"> | – | yes | Options: data-slean-position (top-start … bottom-end), data-slean-duration in milliseconds. |
| list | <ol aria-live="polite"> | list | yes | The live region the toasts are added to. |
| item | <li data-variant="success|warning|danger"> | item | no | Created by showToast(). |
| title | <p> | title | no | Created by showToast(). |
| description | <p> | description | no | Created when a description is passed. |
| action | <button type="button" data-slean-value="…"> | action | no | Created when an action is passed; reports its value in slean:action. |
| close | <button type="button" aria-label="Dismiss"> | close | no | Created on every toast. |
Runtime profile
The toast registers five handlers (click, pointerover, pointerout, focusin, focusout) with the shared router. Its per-instance state is one timer entry per visible toast; showing fifty toasts adds no listener (tests/toast.test.ts).
Accessibility contract
- The list is
aria-live="polite": a new toast is announced after the current speech, and focus never moves. - The region is shown one frame before the first toast is added, so the live region exists before its content changes.
- Hover or focus inside the region pauses every timer (WCAG 2.2.1); a toast with duration 0 stays until closed.
- The close button is named “Dismiss” (
closeLabelchanges it); the action is a native button with its label. - For errors that must interrupt, use a second region whose list has
role="alert".
Keyboard
| Key | When | Result |
|---|---|---|
| Tab | focus moves into the region | Reaches the action and close buttons; timers pause |
| Enter/Space | on a toast button | Runs the action or dismisses (native activation) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
Popover API (manual) | Baseline 2024 (Chrome 114, Safari 17, Firefox 125) | The region renders in place; toasts are still added and announced |
ARIA live regions | Widely supported by assistive technology | Not applicable |
Without JavaScript
No toasts. Feedback that must survive without JavaScript belongs in the page, rendered by the server (the Alert primitive).
Server rendering
Render the empty region. showToast() is a browser call; nothing is created on the server.
Before hydration
Before the behavior loads there is nothing to show; the region is empty and closed.
Styling
toast.css places the region in one of six positions (data-slean-position), stacks the toasts, tints them from the soft tokens by tone, draws the close cross with borders and slides a new toast in. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-space-4 | 1rem | inset-block-end, inset-inline-end, inline-size, max-block-size, inset-block-start, inset-inline-start, padding-inline |
--slean-fg | var(--slean-neutral-12) | color |
--slean-color-scheme | light | color-scheme |
--slean-space-2 | 0.5rem | gap, padding-inline, translate |
--slean-space-3 | 0.75rem | column-gap, padding-block |
--slean-space-1 | 0.25rem | row-gap, margin-block-start, padding-block |
--slean-border | var(--slean-neutral-6) | border |
--slean-radius-md | 0.625rem | border-radius |
--slean-surface | oklch(100% 0 0) | background |
--slean-shadow-md | 0 4px 12px oklch(0% 0 0 / 0.1), 0 1px 3px oklch(0% 0 0 / 0.08) | box-shadow |
--slean-text-sm | 0.875rem | font-size |
--slean-leading | 1.5 | line-height |
--slean-duration-normal | 160ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-success | oklch(55% 0.15 150) | border-color |
--slean-success-soft | oklch(95.5% 0.04 150) | background |
--slean-success-soft-fg | oklch(40% 0.12 150) | color |
--slean-warning | oklch(76% 0.16 80) | border-color |
--slean-warning-soft | oklch(96% 0.05 85) | background |
--slean-warning-soft-fg | oklch(45% 0.12 75) | color |
--slean-danger | oklch(55% 0.2 25) | border-color |
--slean-danger-soft | oklch(95.5% 0.03 25) | background |
--slean-danger-soft-fg | oklch(45% 0.18 25) | color |
--slean-font-weight-semibold | 600 | font-weight |
--slean-radius-sm | 0.375rem | border-radius |
--slean-font-weight-medium | 500 | font-weight |
--slean-control-height-sm | 2rem | inline-size, block-size |
--slean-icon-close | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 4.5l7 7m0-7l-7 7' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") | mask |
--slean-muted | var(--slean-neutral-3) | background |
State selectors the stylesheet targets, all from the platform or ARIA: :hover.
Variant attributes: data-variant (success, warning, danger).
Compatibility notes
The Popover API is Baseline 2024. Without it the region renders in place at its position, and toasts are still added, timed and announced.
Testing
packages/primitives/tests/toast.test.tsVitest: creation, timers, pause and resume, close, action, cancelable dismissalapps/playground/tests/primitives/widgets.spec.tsPlaywright: tooltip, toast, tree, range slider and number field in Chrome, with axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/toast/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/toast/contract.tstyped constants: name, tier, base, parts, options, eventspackages/primitives/src/toast/behavior.tsthe behavior definitionpackages/primitives/src/toast/register.tsthe registration modulepackages/styles/css/toast.cssthe optional stylesheet