Toast
A non-blocking status message. Lives in a corner region, auto-dismisses, and pauses on hover.
Setup
Mount one .toast-region in your app shell so the helper has somewhere to render. Tag it with data-stisla-toast-region="default" and Stisla.toast(...) will land there when no region opt is passed.
<!-- Drop once in your layout, near the end of <body>. -->
<div class="toast-region toast-region--top-center"
data-stisla-toast-region="default"
role="region"
aria-label="Notifications"></div>If you skip the snippet above, Stisla.toast(...) creates the same region itself the first time it runs and appends it to <body>. That works as a fallback, but mounting it yourself keeps the region where you expect it in the DOM and lets you tag it with your own ARIA label.
For apps that want toasts in more than one corner (say, system errors at top-end and chat pings at bottom-start), mount one region per corner, give each a unique data-stisla-toast-region name, then target it from JS with the region opt.
<div class="toast-region toast-region--top-end"
data-stisla-toast-region="alerts"
role="region" aria-label="System alerts"></div>
<div class="toast-region toast-region--bottom-start"
data-stisla-toast-region="chat"
role="region" aria-label="Chat messages"></div>Stisla.toast({
title: "Build failed",
variant: "danger",
region: '[data-stisla-toast-region="alerts"]',
});
Stisla.toast({
title: "Maya: hey there",
region: '[data-stisla-toast-region="chat"]',
});How it works
Two pieces, two lifetimes. The .toast-region is position: fixed, carries the corner placement and the live-region ARIA wrapper, and persists for the page. Individual .toast nodes come and go inside it; they're created by markup or by the JS helper, opened, paused on hover, then dismissed when the user clicks close or the auto-hide timer fires.
State lives on the toast root as data-state="open|closed". Closed toasts are display: none so they don't reserve layout space and a stack of pending markup-first toasts doesn't push the next-to-open one down the page. Hovering or focusing an open toast pauses its auto-hide timer (per WCAG 2.2.1) and leaving resumes it. Re-triggering an already-open toast restarts the timer rather than stacking a duplicate.
Two ways to create a toast:
- Markup-first. Author a
.toastinside the region withdata-state="closed", then open it with adata-stisla-toast-trigger="<id>"button. The toast stays in the DOM and can be re-triggered. See the Trigger section below. - Imperative. Call
Stisla.toast({ title, description, variant }). The helper builds the node, mounts it, opens it, and removes it from the DOM after close. See the JS helper section below.
Basic
A toast carries a leading icon, a title, and an optional body. The icon column is required so the toast keeps a consistent visual anchor. Use a Lucide icon, an .icon-box, or an image. Drop one inside a .toast-region to position it; the region holds the stack and the corner placement. For static demos like this one, the toast sits inline with data-state="open" so it stays visible.
<div class="toast" data-state="open" role="status" aria-live="polite" aria-atomic="true">
<div class="toast__icon"><i data-lucide="bell"></i></div>
<div class="toast__content">
<div class="toast__header">Workspace saved</div>
<div class="toast__body">Your changes to billing details have been written and synced.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close">
<i data-lucide="x"></i>
</button>
</div>Anatomy
The row has three columns. A .toast__icon leading slot, a .toast__content vertical stack, and a .toast__close chip. The content stack carries .toast__header with an optional .toast__timestamp, .toast__body for the description, and .toast__action for inline buttons. The stack owns its own gap so spacing stays consistent regardless of which parts are present.
<div class="toast" data-state="open" role="status" aria-live="polite" aria-atomic="true">
<div class="toast__icon"><i data-lucide="shield-check"></i></div>
<div class="toast__content">
<div class="toast__header">
Two-factor enabled
<span class="toast__timestamp">just now</span>
</div>
<div class="toast__body">Logins to this workspace now require a code from your authenticator app.</div>
<div class="toast__action">
<button type="button" class="btn btn--sm btn--ghost btn--neutral btn--flush-start">Dismiss</button>
<button type="button" class="btn btn--sm btn--ghost btn--neutral">Manage devices</button>
</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close">
<i data-lucide="x"></i>
</button>
</div>Intent variants
Five modifiers shift the leading-icon color. .toast--primary, .toast--success, .toast--warning, .toast--danger, and .toast--info. The surface stays neutral so stacked toasts of mixed intent read as one family. For danger toasts, bump the live region to role="alert" and aria-live="assertive" so screen readers announce them immediately.
<div class="d-flex flex-column gap-3">
<div class="toast toast--success" data-state="open" role="status" aria-live="polite" aria-atomic="true">
<div class="toast__icon"><i data-lucide="check-circle-2"></i></div>
<div class="toast__content">
<div class="toast__header">Invoice sent</div>
<div class="toast__body">We emailed the May invoice to billing@acme.co.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast toast--danger" data-state="open" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast__icon"><i data-lucide="x-circle"></i></div>
<div class="toast__content">
<div class="toast__header">Couldn't upload</div>
<div class="toast__body">Network dropped halfway through. Reconnect and try again.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast toast--warning" data-state="open" role="status" aria-live="polite" aria-atomic="true">
<div class="toast__icon"><i data-lucide="triangle-alert"></i></div>
<div class="toast__content">
<div class="toast__header">Storage almost full</div>
<div class="toast__body">You're at 94% of the workspace quota. Archive old exports to free space.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast toast--info" data-state="open" role="status" aria-live="polite" aria-atomic="true">
<div class="toast__icon"><i data-lucide="info"></i></div>
<div class="toast__content">
<div class="toast__header">New version available</div>
<div class="toast__body">Refresh to pick up the latest dashboard build.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast toast--primary" data-state="open" role="status" aria-live="polite" aria-atomic="true">
<div class="toast__icon"><i data-lucide="bell"></i></div>
<div class="toast__content">
<div class="toast__header">Reminder set</div>
<div class="toast__body">We'll ping you 10 minutes before the meeting.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
</div>Leading slot
The icon column takes any leading visual. Drop in an avatar, a glyph at full tone, an .icon-box, a spinner, or an emoji. Override --toast-icon-size to grow the slot for heavier visuals, and --toast-icon-color to recolor monochrome content.
<div class="d-flex flex-column gap-3">
<div class="toast" data-state="open" role="status" aria-live="polite" aria-atomic="true" style="--toast-icon-size: 2.25rem;">
<div class="toast__icon fs-1 fw-semibold" style="background-color: #6366f1; color: white; border-radius: 50%;">MS</div>
<div class="toast__content">
<div class="toast__header">
Maya Singh
<span class="toast__timestamp">10 mins ago</span>
</div>
<div class="toast__body">Shared "Q3 roadmap" with you. You have edit access to the document.</div>
<div class="toast__action">
<button type="button" class="btn btn--sm btn--ghost btn--neutral btn--flush-start">Dismiss</button>
<button type="button" class="btn btn--sm btn--ghost btn--primary">Open file</button>
</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast" data-state="open" role="status" aria-live="polite" aria-atomic="true" style="--toast-icon-color: var(--st-foreground);">
<div class="toast__icon"><i data-lucide="git-pull-request"></i></div>
<div class="toast__content">
<div class="toast__header">Review requested</div>
<div class="toast__body">Maya tagged you on "fix(auth): rotate session keys".</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast" data-state="open" role="status" aria-live="polite" aria-atomic="true" style="--toast-icon-size: 2.25rem;">
<div class="toast__icon">
<span class="icon-box icon-box--primary"><i data-lucide="rocket"></i></span>
</div>
<div class="toast__content">
<div class="toast__header">Deployment succeeded</div>
<div class="toast__body">v2.4.1 is live on staging. Production rollout queued for 4 PM.</div>
<div class="toast__action">
<button type="button" class="btn btn--sm btn--ghost btn--primary btn--flush-start">View deploy</button>
</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast" data-state="open" role="status" aria-live="polite" aria-atomic="true">
<div class="toast__icon">
<span class="spinner spinner--sm" role="status" aria-label="Loading"></span>
</div>
<div class="toast__content">
<div class="toast__header">
Uploading 3 files
<span class="toast__timestamp">62%</span>
</div>
<div class="toast__body">Don't close this window until upload completes.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast" data-state="open" role="status" aria-live="polite" aria-atomic="true" style="--toast-icon-size: 1.5rem;">
<div class="toast__icon fs-5 lh-1">🎉</div>
<div class="toast__content">
<div class="toast__header">First sale!</div>
<div class="toast__body">Pro plan subscription from sarah@acme.co. Cha-ching.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
</div>Trigger
Markup-first toasts always live inside a .toast-region. The region is position: fixed and pins to a viewport corner. The trigger button then points at a toast by id, calls open(), and starts the autohide timer; re-clicking restarts the timer. Use data-stisla-toast-autohide and data-stisla-toast-delay on the toast root to retune per-instance.
<button type="button" class="btn btn--primary" data-stisla-toast-trigger="triggerToast">Show toast</button>
<div class="toast-region toast-region--top-end" role="region" aria-label="Notifications">
<div class="toast" id="triggerToast"
data-stisla-toast
data-stisla-toast-delay="4000"
data-state="closed"
role="status" aria-live="polite" aria-atomic="true" aria-hidden="true">
<div class="toast__icon"><i data-lucide="link"></i></div>
<div class="toast__content">
<div class="toast__header">Link copied</div>
<div class="toast__body">Anyone with the link can view the page.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
</div>Auto-hide and persistent
Auto-hide is on by default with a 4-second delay. Hovering or focusing the toast pauses the timer (WCAG 2.2.1) and leaving resumes it. Set data-stisla-toast-autohide="false" for a sticky toast that only the close button dismisses, useful for errors and confirmations.
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-toast-trigger="autoHideToast">Auto-hide (4s)</button>
<button type="button" class="btn btn--outline btn--neutral" data-stisla-toast-trigger="persistentToast">Persistent</button>
</div>
<div class="toast-region toast-region--top-end" role="region" aria-label="Notifications">
<div class="toast toast--success" id="autoHideToast"
data-stisla-toast
data-stisla-toast-delay="4000"
data-state="closed"
role="status" aria-live="polite" aria-atomic="true" aria-hidden="true">
<div class="toast__icon"><i data-lucide="check-circle-2"></i></div>
<div class="toast__content">
<div class="toast__header">Saved</div>
<div class="toast__body">Hover to pause the timer.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
<div class="toast toast--danger" id="persistentToast"
data-stisla-toast
data-stisla-toast-autohide="false"
data-state="closed"
role="alert" aria-live="assertive" aria-atomic="true" aria-hidden="true">
<div class="toast__icon"><i data-lucide="x-circle"></i></div>
<div class="toast__content">
<div class="toast__header">Couldn't sync</div>
<div class="toast__body">Dismiss manually. The toast stays until you close it.</div>
</div>
<button type="button" class="toast__close" data-stisla-toast-dismiss aria-label="Close"><i data-lucide="x"></i></button>
</div>
</div>Position
Six modifiers on .toast-region set the corner. --top-start, --top-center (default), --top-end, --bottom-start, --bottom-center, and --bottom-end. Bottom regions stack newest-first at the bottom edge. Name each region with data-stisla-toast-region so Stisla.toast({ region }) can target it.
<div class="d-grid gap-2" style="grid-template-columns: repeat(3, 1fr);">
<button type="button" class="btn btn--outline btn--neutral" data-demo-toast-position="top-start">Top start</button>
<button type="button" class="btn btn--outline btn--neutral" data-demo-toast-position="top-center">Top center</button>
<button type="button" class="btn btn--outline btn--neutral" data-demo-toast-position="top-end">Top end</button>
<button type="button" class="btn btn--outline btn--neutral" data-demo-toast-position="bottom-start">Bottom start</button>
<button type="button" class="btn btn--outline btn--neutral" data-demo-toast-position="bottom-center">Bottom center</button>
<button type="button" class="btn btn--outline btn--neutral" data-demo-toast-position="bottom-end">Bottom end</button>
</div>JS helper
Stisla.toast(opts) builds the toast node, mounts it into a region, opens it, and removes it from the DOM after close. Returns the Toast instance so callers can .close() manually. If no region exists in the page, the helper appends a default top-center region to <body> on first call (see Setup).
// Object-form
Stisla.toast({
title: "Saved",
description: "Your changes are saved.",
variant: "success",
action: { label: "Undo", onClick: () => undoLastEdit() },
delay: 4000,
});
// Named shortcuts — same opts, intent + default icon prefilled
Stisla.toast.success("Saved");
Stisla.toast.error("Couldn't upload", { description: "Network dropped." });
Stisla.toast.warning("Storage almost full");
Stisla.toast.info("New version available");<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn--ghost btn--neutral" data-demo-toast-shortcut="success">Success</button>
<button type="button" class="btn btn--ghost btn--neutral" data-demo-toast-shortcut="error">Error</button>
<button type="button" class="btn btn--ghost btn--neutral" data-demo-toast-shortcut="warning">Warning</button>
<button type="button" class="btn btn--ghost btn--neutral" data-demo-toast-shortcut="info">Info</button>
<button type="button" class="btn btn--primary" data-demo-toast-action>With action</button>
</div>Promise
Stisla.toast.promise() shows a loading toast (spinner icon, autohide off), then patches it in place when the promise settles. The success and error fields accept strings or functions of (data) / (err). The original promise is not intercepted. Callers still await and .catch() as usual.
Stisla.toast.promise(savePromise, {
loading: "Saving changes…",
success: (data) => `Saved ${data.name}`,
error: (err) => `Failed: ${err.message}`,
delay: 4000, // post-settle autohide window
});<button type="button" class="btn btn--primary" data-demo-toast-promise>Run promise demo</button>Customization
Twenty-eight variables retune .toast and .toast-region without touching component CSS. Override on a single instance, on the region, on a wrapper, or on :root. Intent modifiers shift only --toast-icon-color, so the surface stays neutral and a stack of mixed intents reads as one family.
Region geometry
| Variable | Default | Use |
|---|---|---|
--toast-region-inset | 1rem | Distance from viewport edges |
--toast-region-gap | 1rem | Vertical space between stacked toasts |
--toast-region-max-width | 24rem | Region cap so wide viewports don't stretch the stack |
--toast-region-z-index | 1090 | Above dialog (1055) so a toast can confirm a dialog action |
Geometry
| Variable | Default | Use |
|---|---|---|
--toast-min-width | 18rem | Floor so short titles still feel substantial |
--toast-max-width | 21.875rem | 350px ceiling so long bodies wrap before getting unwieldy |
--toast-padding | 1rem | Single frame pad on the root |
--toast-radius | var(--st-radius-lg) | Large-frame tier (matches dialog and card) |
--toast-z-index | 1090 | Individual toast stacking |
Surface
| Variable | Default | Use |
|---|---|---|
--toast-bg | var(--st-surface) | Card-tier surface |
--toast-color | var(--st-foreground) | Title color |
--toast-border-color | var(--st-border) | Opaque border (not the translucent variant) |
--toast-shadow | var(--st-shadow) | Default ambient shadow |
Layout
| Variable | Default | Use |
|---|---|---|
--toast-column-gap | 0.75rem | Grid column gap between icon, content, and close |
--toast-content-gap | 0.25rem | Vertical gap inside the content stack (title, body, action) |
Header
| Variable | Default | Use |
|---|---|---|
--toast-header-font-weight | 600 | Title weight |
--toast-header-font-size | 0.875rem | Title size |
Body
| Variable | Default | Use |
|---|---|---|
--toast-body-color | var(--st-muted-foreground) | Description text color |
--toast-body-font-size | 0.875rem | Description size |
Icon
| Variable | Default | Use |
|---|---|---|
--toast-icon-size | 1.125rem | Leading icon footprint |
--toast-icon-color | var(--st-muted-foreground) | Intent modifiers retune this var and nothing else |
Close chip
| Variable | Default | Use |
|---|---|---|
--toast-close-size | 1.5rem | Tap target footprint |
--toast-close-color | var(--st-muted-foreground) | Rest icon color |
--toast-close-color-hover | var(--st-foreground) | Icon color on hover/focus |
--toast-close-bg-hover | var(--st-accent) | Chip fill on hover/focus |
Action and motion
| Variable | Default | Use |
|---|---|---|
--toast-action-gap | 0.25rem | Space between action-row buttons |
--toast-transition-duration | 0.2s | Open and close fade plus slide |
--toast-enter-transform | translateY(-0.5rem) | Direction the toast slides from on entry (bottom regions flip to translateY(0.5rem)) |