sveltelean Primitives
Versionv0.2.0 GitHub

Example

Three toasts
    <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>
    The first toast has an action, the second only a title, the third is an error that stays until it is closed (duration 0). Hover the stack: every timer pauses.

    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/primitives
    +layout.svelte or any client module
    import '@svelte-lean/primitives/toast/register';
    stylesheets
    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.

    keep-errors.svelte
    <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

    PartElementdata-slean-partRequiredNotes
    root<section data-slean="toast" popover="manual" aria-label="Notifications">–yesOptions: data-slean-position (top-start … bottom-end), data-slean-duration in milliseconds.
    list<ol aria-live="polite">listyesThe live region the toasts are added to.
    item<li data-variant="success|warning|danger">itemnoCreated by showToast().
    title<p>titlenoCreated by showToast().
    description<p>descriptionnoCreated when a description is passed.
    action<button type="button" data-slean-value="…">actionnoCreated when an action is passed; reports its value in slean:action.
    close<button type="button" aria-label="Dismiss">closenoCreated 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” (closeLabel changes 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

    KeyWhenResult
    Tabfocus moves into the regionReaches the action and close buttons; timers pause
    Enter/Spaceon a toast buttonRuns the action or dismisses (native activation)

    Platform features

    FeatureBaselineOutside 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 regionsWidely supported by assistive technologyNot 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:

    TokenDefault (light)Applies to
    --slean-space-41reminset-block-end, inset-inline-end, inline-size, max-block-size, inset-block-start, inset-inline-start, padding-inline
    --slean-fgvar(--slean-neutral-12)color
    --slean-color-schemelightcolor-scheme
    --slean-space-20.5remgap, padding-inline, translate
    --slean-space-30.75remcolumn-gap, padding-block
    --slean-space-10.25remrow-gap, margin-block-start, padding-block
    --slean-bordervar(--slean-neutral-6)border
    --slean-radius-md0.625remborder-radius
    --slean-surfaceoklch(100% 0 0)background
    --slean-shadow-md0 4px 12px oklch(0% 0 0 / 0.1), 0 1px 3px oklch(0% 0 0 / 0.08)box-shadow
    --slean-text-sm0.875remfont-size
    --slean-leading1.5line-height
    --slean-duration-normal160mstransition
    --slean-easecubic-bezier(0.2, 0, 0, 1)transition
    --slean-successoklch(55% 0.15 150)border-color
    --slean-success-softoklch(95.5% 0.04 150)background
    --slean-success-soft-fgoklch(40% 0.12 150)color
    --slean-warningoklch(76% 0.16 80)border-color
    --slean-warning-softoklch(96% 0.05 85)background
    --slean-warning-soft-fgoklch(45% 0.12 75)color
    --slean-dangeroklch(55% 0.2 25)border-color
    --slean-danger-softoklch(95.5% 0.03 25)background
    --slean-danger-soft-fgoklch(45% 0.18 25)color
    --slean-font-weight-semibold600font-weight
    --slean-radius-sm0.375remborder-radius
    --slean-font-weight-medium500font-weight
    --slean-control-height-sm2reminline-size, block-size
    --slean-icon-closeurl("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-mutedvar(--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

    Source