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

ModeUse 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().
MessageFeedback that belongs next to a section or form, not floating over it.
BannerPersistent whole-page state rather than a passing event.

Examples

Appearances

All four appearances. The danger one passes duration: 0 (the shorthand for manual close) because failures should be acknowledged.

Close modes

'auto' goes by itself (hover it to pause the timer); 'manual' is sticky until the ×; the 'never' toast has no ×, so only "Dismiss it" (calling .dismiss()) removes it.

Durations

duration tunes the auto-close timer (default 5000 ms). Hover or Tab into either toast. The countdown pauses and resumes with the remaining time.

bodyHtml

bodyHtml renders markup (bold, links) inside the toast body: the explicit opt-in, for trusted content only.

on('dismiss')

Dismissed so far (× or timeout): 0. Each toast's on('dismiss') listener bumps the counter.

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.maxVisible tunes 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 on mouseenter AND focusin, 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 get role="status" and queue politely.
  • Exit: dismissing adds .soma-toast-closing, waits for transitionend, then removes the element; a 250 ms timeout fallback covers prefers-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.

ClassEffect
.soma-toastsThe 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-toastOne 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 / -closeBody wrapper, bold title paragraph, dismiss button.
.soma-toast-closingAdded on dismiss for the exit transition; a timeout fallback removes the element under reduced motion.

JavaScript

Factory + options

MemberDescription
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.maxVisibleThe visible-stack cap (default 3), assignable at runtime for consumers with unusual real estate.
opts.titleOptional bold first line. Inserted as text — never parsed as HTML.
opts.bodyOptional body paragraph, also inserted as text.
opts.bodyHtmlMarkup 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.durationAuto-close delay in ms (default 5000). Only meaningful in 'auto' mode; 0 is the long-standing shorthand for close: 'manual'.

Return value + events

MemberDescription
.elThe 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); });
});