Toast
Notification messages that appear temporarily. Managed imperatively via createToaster(). Supports auto-dismiss, pause on hover/focus, swipe-to-dismiss, keyboard hotkey, promise tracking, and ARIA live regions.
Anatomy
<script>
import { Toast, createToaster } from '$soma/components/toast';
const toaster = createToaster({ duration: 5000 });
</script>
<Toast.Provider {toaster}>
<Toast.Viewport>
{#each toaster.toasts as t (t.id)}
<Toast.Item toast={t}>
<Toast.Title>{t.title}</Toast.Title>
<Toast.Description>{t.description}</Toast.Description>
<Toast.Action altText="Undo action" onclick={t.action?.onClick}>
{t.action?.label}
</Toast.Action>
<Toast.Close>x</Toast.Close>
</Toast.Item>
{/each}
</Toast.Viewport>
</Toast.Provider>
Setup the Provider once in a layout. Create toasts from anywhere via the toaster instance.
Parts
| Part |
Element |
Description |
Provider |
none |
Root context. Receives the toaster instance. No DOM. |
Viewport |
<div> |
Positioned container for all toasts. ARIA region. Hotkey target. |
Item |
<div> |
Individual toast. Auto-dismiss timer, pause on hover/focus, swipe. |
Title |
<div> |
Toast heading. Linked via aria-labelledby. |
Description |
<div> |
Toast body text. Linked via aria-describedby. |
Action |
<button> |
Interactive action button with accessible description. |
Close |
<button> |
Dismiss button. |
Imperative API
const toaster = createToaster({
duration: 5000, // default auto-dismiss (ms), 0 for none
max: 5, // max visible toasts
swipeDirection: 'right', // 'left' | 'right' | 'up' | 'down'
swipeThreshold: 50, // px distance to trigger dismiss
hotkey: ['F8'] // keyboard shortcut to focus viewport
});
// Create toasts by semantic intent
const id = toaster.create({ title: 'Hello' });
toaster.create({ title: 'Saved', description: 'Changes saved.', intent: 'fulfill' });
toaster.create({ title: 'Something needs attention', intent: 'risk' });
toaster.create({ title: 'Something went wrong', intent: 'threat' });
toaster.create({ title: 'Uploading...', loading: true }); // persistent by default (duration: 0)
// Manage toasts
toaster.dismiss(id); // dismiss one
toaster.dismiss(); // dismiss all
toaster.update(id, { title: 'Updated' });
// Toast with action
toaster.create({
title: 'File deleted',
action: { label: 'Undo', onClick: () => restore() }
});
// Toast with custom duration
toaster.create({ title: 'Quick', duration: 2000 });
toaster.create({ title: 'Persistent', duration: 0 }); // no auto-dismiss
// Promise tracking — loading → fulfill/threat
toaster.promise(fetchData(), {
loading: { title: 'Loading...', description: 'Fetching data.' },
fulfill: (data) => ({ title: 'Done', description: `Loaded ${data.count} items.` }),
threat: (err) => ({ title: 'Failed', description: String(err) })
});
ARIA
| Part |
Attribute |
Value |
| Viewport |
role |
region |
| Viewport |
aria-label |
Configurable (default "Notifications") |
| Item |
role |
status (neutral/affirm/fulfill) | alert (risk/threat) |
| Item |
aria-live |
polite (neutral/affirm/fulfill) | assertive (risk/threat) |
| Item |
aria-atomic |
true |
| Item |
aria-labelledby |
ID of Title |
| Item |
aria-describedby |
ID of Description |
| Action |
aria-label |
altText prop value |
| Close |
aria-label |
Translated "Close" |
Risk and threat toasts use role="alert" with aria-live="assertive" for immediate screen reader announcement. Other intents use role="status" with aria-live="polite".
Data Attributes
Provider is pure context — it emits no DOM, so it has no data-* attribute.
| Part |
Attribute |
Values |
| Viewport |
data-toast-viewport |
Always present |
| Item |
data-toast-item |
Always present |
| Item |
data-state |
open | closed |
| Item |
data-intent |
neutral | affirm | fulfill | risk | threat |
| Item |
data-loading |
Present while the toast is in loading/pending phase |
| Item |
data-swipe |
start | move | cancel | end (during swipe gesture) |
| Item |
data-swipe-direction |
left | right | up | down |
| Item |
data-starting-style |
Present during open animation |
| Item |
data-ending-style |
Present during close animation |
| Title |
data-toast-title |
Always present |
| Description |
data-toast-description |
Always present |
| Action |
data-toast-action |
Always present |
| Close |
data-toast-close |
Always present |
CSS Variables
| Variable |
Part |
Description |
--soma-toast-swipe-move-x |
Item |
Horizontal swipe displacement in px |
--soma-toast-swipe-move-y |
Item |
Vertical swipe displacement in px |
--soma-toast-swipe-end-x |
Item |
Final horizontal position on dismiss (e.g. 100%) |
--soma-toast-swipe-end-y |
Item |
Final vertical position on dismiss (e.g. 100%) |
Keyboard
| Key |
Action |
F8 (default hotkey) |
Focus the toast viewport |
Tab |
Navigate between action/close buttons within a toast |
The hotkey is configurable via createToaster({ hotkey: ['F8'] }).
Behavior
Auto-dismiss
- Default duration: 5000ms (configurable per toaster and per toast)
- Set
duration: 0 on a toast to disable auto-dismiss
loading: true defaults to duration: 0 (persistent until dismissed)
- Timer pauses on hover (
pointerenter) and focus
- Timer resumes on
pointerleave and blur
Swipe to dismiss
- Swipe direction configurable:
left, right, up, down (default right)
- Threshold configurable (default 50px)
- During swipe:
data-swipe="move" + CSS variables for position
- On cancel:
data-swipe="cancel", position resets
- On dismiss:
data-swipe="end", toast removed
Max toasts
- When
max is exceeded, the oldest toast is dismissed automatically
- Dismissed toast's
onDismiss callback is called
Promise tracking
toaster.promise(promise, { loading, fulfill, threat }) creates a loading toast
- When the promise resolves, the toast updates to
intent='fulfill'
- When the promise rejects, the toast updates to
intent='threat'
fulfill/threat options can be functions receiving the resolved value or error
Usage
Setup in layout
<!-- +layout.svelte -->
<script>
import { Toast, createToaster } from '$soma/components/toast';
import { setContext } from 'svelte';
const toaster = createToaster({ duration: 5000, max: 5 });
setContext('toaster', toaster);
</script>
<Toast.Provider {toaster}>
<slot />
<Toast.Viewport>
{#each toaster.toasts as t (t.id)}
<Toast.Item toast={t}>
<Toast.Title>{t.title}</Toast.Title>
{#if t.description}
<Toast.Description>{t.description}</Toast.Description>
{/if}
<Toast.Close>x</Toast.Close>
</Toast.Item>
{/each}
</Toast.Viewport>
</Toast.Provider>
Create toast from any page
<script>
import { getContext } from 'svelte';
const toaster = getContext('toaster');
</script>
<button onclick={() => toaster.create({ title: 'Saved!', intent: 'fulfill' })}> Save </button>
Promise tracking
<button
onclick={() => {
toaster.promise(saveData(), {
loading: { title: 'Saving...' },
fulfill: () => ({ title: 'Saved!' }),
threat: (e) => ({ title: 'Failed', description: e.message })
});
}}
>
Save
</button>
Swipe animation CSS
[data-toast-item] {
transition: transform 200ms ease;
}
[data-toast-item][data-swipe='move'] {
transform: translateX(var(--soma-toast-swipe-move-x));
transition: none;
}
[data-toast-item][data-swipe='cancel'] {
transform: translateX(0);
}
Comparison with reference libraries
| Feature |
Radix |
Ark |
Sonner |
Soma |
| API style |
Declarative |
Imperative |
Imperative |
Imperative |
| Semantic variants |
foreground/background |
success/error/warning/info |
success/error/etc |
intent + loading state |
| Auto-dismiss |
Yes |
Yes |
Yes |
Yes |
| Pause on hover |
Yes |
Yes |
Yes |
Yes |
| Swipe to dismiss |
Yes |
Yes |
Yes |
Yes |
| Swipe CSS vars |
Yes |
No |
No |
Yes |
| Max toasts |
Manual |
Built-in |
Built-in |
Built-in |
| Hotkey |
F8 |
No |
Alt+T |
F8 (configurable) |
| Promise tracking |
No |
Yes |
Yes |
Yes |
| Loading state |
No |
No |
Yes |
Yes |
| ARIA urgency |
Manual |
Auto by type |
Auto |
Auto by intent |
| Queue management |
Manual |
Built-in |
Built-in |
Built-in |