Toasts
Summary
Transient notifications created imperatively with
Soma.toast() — there is no authored markup. Toasts
stack top-right above every overlay, auto-dismiss by default
(the timer pauses while the pointer or keyboard focus is inside,
so nobody loses one mid-sentence), and play a short exit
animation on dismiss (skipped under reduced motion). Warning and
danger toasts interrupt with role="alert"; info and
success queue politely. Strings are inserted as text.
bodyHtml is the explicit markup opt-in.
When to use
| Mode | Use it for |
|---|---|
close: 'auto' (default) | Routine confirmations of async events: deploy finished, settings saved. Gone in duration ms. |
close: 'manual' | Failures the user must acknowledge — sticky until the × is clicked. duration: 0 is shorthand. |
close: 'never' | Programmatic lifecycles ("reconnecting…"). No close button; your code calls .dismiss(). |
| Message | Feedback that belongs next to a section or form, not floating over it. |
| Banner | Persistent whole-page state rather than a passing event. |
Examples
Timing, stacking & accessibility
The first toast creates a single .soma-toasts stack
container, pinned to the top inline-end corner (top-right in LTR,
top-left in RTL) with aria-live="polite"; every
later toast appends to it, newest at the bottom. The container
is a popover="manual" shown in the browser's
top layer while toasts are visible, so toasts
paint above every overlay including open native
dialogs — something no
z-index could guarantee. One platform nuance:
while a modal dialog is open the rest of the page (toasts
included) is inert, so a toast stays readable above the dialog
but its × only becomes clickable once the dialog closes (auto
timers keep running).
A second, related top-layer characteristic: open recency orders the top layer. The toast container enters the top layer when the first toast shows and leaves when the stack empties, so a dialog or popover opened while toasts are already visible paints above them until the stack empties and refills. This is accepted, known behaviour: re-fronting would mean hiding and re-showing the container, which re-announces the whole live region to screen readers, a far worse trade than a temporarily covered corner.
- Queue: at most three toasts are on screen at once (
Soma.toast.maxVisibletunes the cap); further calls wait in a FIFO queue and appear — and announce — as visible ones dismiss, whichever way (timer, ×, or.dismiss()). A queued toast's handle works normally:.dismiss()before it shows removes it from the queue and still fires its listeners, and its auto timer only starts once it is actually visible. - Changing the cap: assign once at startup, before spawning toasts:
// Default is 3 — the recommended cap for operational consoles // (an event storm queues instead of filling the screen). Soma.toast.maxVisible = 5; // roomier dashboards Soma.toast.maxVisible = Infinity; // no cap: every toast stacks // immediately (Neura-style) - Auto timer: runs only in
close: 'auto'mode. It pauses onmouseenterANDfocusin, and resumes with the remaining time on leave/blur. A keyboard user can Tab into a toast and read it without a countdown. - Roles: warning/danger toasts get
role="alert"and interrupt; info/success getrole="status"and queue politely. - Exit: dismissing adds
.soma-toast-closing, waits fortransitionend, then removes the element; a 250 ms timeout fallback coversprefers-reduced-motion(no transition → no event). The enter animation is likewise skipped under reduced motion.
HTML
None to author — toasts are built by the component. The generated structure, for theming reference:
<div class="soma-toasts" aria-live="polite" popover="manual">
<div class="soma-toast soma-toast-warning" role="alert">
<span class="soma-icon soma-icon-triangle-alert"></span>
<div class="soma-toast-content">
<p class="soma-toast-title">Budget warning</p>
<p>82% of monthly budget used</p>
</div>
<button class="soma-toast-close" aria-label="Dismiss">
<span class="soma-icon soma-icon-close"></span>
</button>
</div>
</div>
The close button appears in 'auto' and
'manual' modes (its label is localised via the
Soma.i18n key toast.dismiss); a
'never' toast renders without it. The icon follows the
appearance: info → info, success → check,
warning → triangle-alert, danger →
circle-alert.
CSS classes
All generated by the component; listed for theming reference.
| Class | Effect |
|---|---|
.soma-toasts | The stack container, pinned to the top inline-end corner (aria-live="polite"). A popover="manual" shown in the top layer while toasts are visible — above open dialogs. Created once, reused by every toast. |
.soma-toast | One toast. Appearance variants: -info, -success, -warning, -danger. Each tints the icon and paints a 4px inline-start accent bar in its semantic color; the body stays the neutral surface. |
.soma-toast-content / -title / -close | Body wrapper, bold title paragraph, dismiss button. |
.soma-toast-closing | Added on dismiss for the exit transition; a timeout fallback removes the element under reduced motion. |
JavaScript
Factory + options
| Member | Description |
|---|---|
Soma.toast(opts) | Create and show a toast — or queue it when the visible cap (default three) is already full; returns the handle below either way. Throws without at least one of title / body / bodyHtml, and on unknown appearance or close values. |
Soma.toast.maxVisible | The visible-stack cap (default 3), assignable at runtime for consumers with unusual real estate. |
opts.title | Optional bold first line. Inserted as text — never parsed as HTML. |
opts.body | Optional body paragraph, also inserted as text. |
opts.bodyHtml | Markup opt-in for the body (bold, links), for content you control only. Wins over body when both are set; combines with title. |
opts.appearance | 'info' (default) | 'success' | 'warning' | 'danger'. Picks the icon and the live-region role. |
opts.close | 'auto' — dismisses after duration ms, pausing while hovered or focused; 'manual' — sticky, only the × dismisses; 'never' — no × at all, your code calls .dismiss(). When omitted, derived from duration: 'auto' if it is > 0, else 'manual'. |
opts.duration | Auto-close delay in ms (default 5000). Only meaningful in 'auto' mode; 0 is the long-standing shorthand for close: 'manual'. |
Return value + events
| Member | Description |
|---|---|
.el | The toast element, for direct addEventListener or inspection. |
.dismiss() | Play the exit animation, then remove the toast and fire the dismiss listeners. On a still-queued toast it removes it from the queue instead (no animation, listeners still fire). Idempotent — repeated calls (or a call racing the auto timer) are no-ops. |
.on('dismiss', fn) | Run fn after the toast is gone (×, timeout, or programmatic alike). Returns the handle, so calls chain. |
.off('dismiss', fn) | Remove a listener: pass the same function reference given to .on(). Returns the handle, so calls chain. |
The underlying DOM event is a bubbling
soma-toast-dismiss CustomEvent dispatched on the toast
element as dismissal starts (before the exit animation), usable
with document.addEventListener for delegated
listening.
A sticky failure toast. Errors should be acknowledged, not time out:
Soma.toast({
title: 'Deploy failed',
body: 'Step "migrate" exited with code 1.',
appearance: 'danger',
close: 'manual', // sticky until dismissed
});
A programmatic lifecycle — close: 'never' while the
condition lasts, then dismiss and confirm:
const t = Soma.toast({ body: 'Reconnecting…', close: 'never' });
socket.addEventListener('open', () => {
t.dismiss();
Soma.toast({ body: 'Connection restored', appearance: 'success' });
});
Delegated listening via the bubbling CustomEvent — e.g. one analytics hook for every toast on the page:
document.addEventListener('soma-toast-dismiss', (e) => {
console.log('toast dismissed:', e.target.textContent.trim());
});
Tuning the timer — longer for toasts that carry more words:
Soma.toast({ body: 'Settings saved' }); // 5 s default
Soma.toast({ body: 'Long release notes…', duration: 10000 });
Soma.toast({ body: 'Sticky', duration: 0 }); // = close: 'manual'
The live examples above, exactly as this page wires them:
// Appearances
document.getElementById('toast-info').addEventListener('click', () =>
Soma.toast({ title: 'Drift detected', body: '2 resources diverge from nware-cortex.yaml', appearance: 'info' }));
document.getElementById('toast-success').addEventListener('click', () =>
Soma.toast({ title: 'Deploy complete', body: 'api-backend v2.3.1 is live', appearance: 'success' }));
document.getElementById('toast-warning').addEventListener('click', () =>
Soma.toast({ title: 'Budget warning', body: '82% of monthly budget used', appearance: 'warning' }));
document.getElementById('toast-danger').addEventListener('click', () =>
Soma.toast({ title: 'Deploy failed', body: 'Step "migrate" exited with code 1', appearance: 'danger', duration: 0 }));
// Close modes
document.getElementById('toast-auto').addEventListener('click', () =>
Soma.toast({ title: 'Settings saved', body: 'Gone in 5 s — hover to pause', appearance: 'success' }));
document.getElementById('toast-manual').addEventListener('click', () =>
Soma.toast({ title: 'Manual close', body: 'Sticky until you click the ×', appearance: 'info', close: 'manual' }));
let neverToast = null;
document.getElementById('toast-never').addEventListener('click', () => {
if (neverToast) return;
neverToast = Soma.toast({ body: 'Reconnecting…', appearance: 'info', close: 'never' });
neverToast.on('dismiss', () => { neverToast = null; });
});
document.getElementById('toast-never-dismiss').addEventListener('click', () => {
if (neverToast) neverToast.dismiss();
});
// Durations
document.getElementById('toast-quick').addEventListener('click', () =>
Soma.toast({ body: 'Quick — gone in 2 s', appearance: 'info', duration: 2000 }));
document.getElementById('toast-long').addEventListener('click', () =>
Soma.toast({ body: 'Long — 10 s to read this one', appearance: 'info', duration: 10000 }));
// bodyHtml — the markup opt-in
document.getElementById('toast-html').addEventListener('click', () =>
Soma.toast({
title: 'Deploy complete',
bodyHtml: 'api-backend <strong>v2.3.1</strong> is live — <a href="#">release notes</a>',
appearance: 'success',
close: 'manual',
}));
// on('dismiss') → visible counter
const dismissedEl = document.getElementById('toast-dismissed');
let dismissed = 0;
document.getElementById('toast-count').addEventListener('click', () => {
const t = Soma.toast({ title: 'Counted toast', body: 'Dismiss me — or wait.', appearance: 'info' });
t.on('dismiss', () => { dismissedEl.textContent = String(++dismissed); });
});