Skip to content

Toast / snackbar

In review
Live · web

Spawn a toast: they stack, newest on top, and auto-dismiss

0 live · action fired 0 times. Hover or tab a toast to pause its countdown; Esc dismisses it.
// Web: React, consuming @dizyx/nockerl-tokens. NockerlToast ships from
// @dizyx/nockerl-react: ONE transient floating card. The solid lifted surface,
// the leading filled status disc whose ring drains as the countdown, hover/focus
// pause, and Esc-to-dismiss are all in the package. NockerlToast is the single
// toast; the app owns the corner viewport + the stack (spawn / remove / cap).
import { useCallback, useRef, useState } from 'react';
import { NockerlToast, type NockerlToastProps } from '@dizyx/nockerl-react';
// The queued DATA (the toast fields minus the render-time callbacks) plus an id.
type ToastData = Omit<NockerlToastProps, 'onClose' | 'onAction'>;
interface Queued extends ToastData {
id: number;
}
function useToasts() {
const [toasts, setToasts] = useState<Queued[]>([]);
const idRef = useRef(0);
const remove = useCallback((id: number) => setToasts((l) => l.filter((t) => t.id !== id)), []);
const show = useCallback((t: ToastData) => {
const id = ++idRef.current;
setToasts((l) => [...l, { ...t, id }].slice(-3)); // cap the visible stack at 3 (the shared max)
}, []);
return { toasts, show, remove };
}
function SessionActions() {
const { toasts, show, remove } = useToasts();
return (
<>
<button onClick={() => show({ intent: 'info', message: 'A new session opened on api-server.', duration: 5000 })}>
Open
</button>
<button
onClick={() =>
show({
intent: 'error',
title: 'Message failed to send',
message: 'The gateway returned a 502.',
actionLabel: 'Undo',
duration: 0, // 0 → persistent: stays until dismissed
})
}
>
Send
</button>
{/* Mount the stack in a corner viewport (bottom-right here); NockerlToast is one card. */}
<div className="toast-viewport">
{toasts.map((t) => (
<NockerlToast key={t.id} {...t} onClose={() => remove(t.id)} onAction={resend} />
))}
</div>
</>
);
}

NockerlToast renders ONE toast from a NockerlToastProps object plus an onClose (timer / Esc / close-X) and an optional onAction (the action button). The corner viewport + the stack (spawn / remove / cap) are the app’s. Mount the cards where you want them; the toast owns its own countdown, hover/focus pause, and Esc dismissal.

PropTypeDefaultDescription
message *stringBody copy that carries the toast's accessible text (rendered as HTML).
onClose *() => voidDismiss handler, fired when the timer elapses, Esc is pressed, or the close (X) is clicked.
intentNockerlToastIntent'info'Drives the status color, the default icon, and the live role.
titlestringOptional bold heading above the message (one short line).
iconbooleantrueShow the leading status disc (defaults to on).
actionLabelstringOptional quiet text button in the status color (right-aligned; e.g. Undo).
onAction() => voidAction handler (ignored when no actionLabel).
durationnumberAuto-dismiss after this many ms. 0 / undefined → PERSISTENT (no timeout); the toast then shows a "pinned" marker instead of the ring and waits for close.
Toast vs banner vs dialog

Three feedback surfaces, three jobs. Don’t reach for the wrong one:

  • Toast (this page): transient + floating + non-blocking. It appears in a corner, stacks, auto-dismisses on a timer, and never takes focus. For ephemeral confirmations (“Saved”, “Undo?”) and passing errors.
  • Banner: inline + persistent. It sits in the layout and stays until the state clears or the user dismisses it. For ongoing app state (read-only, degraded mode, an approval waiting).
  • Dialog: modal + blocking. It interrupts with a scrim and demands a decision. For destructive confirms and required choices.
Cross-platform drift (tracked for reconciliation)

Web ships NockerlToast (@dizyx/nockerl-react). The Kotlin and Swift tabs document the proposed native shape, designed from the laws plus the transient-message vocabulary the apps already use. The divergences to reconcile when they land, tracked in the Review queue:

  • Precedent differs sharply: Voice has a real transient floating overlay (RecordingHUD, a non-activating NSPanel that auto-hides via scheduleHide, 1.8s for a result / 8.0s for an error, fades 0.22s/0.28s, status-tinted border + neutral shadow, never steals focus). Android’s transient state is inline connection “pills” plus the Inbox sheet rather than a floating card. This page proposes the shared, stackable component both should adopt.
  • info intent color (ratified): info = cyan everywhere. Voice’s token set gains status.info as a cyan alias (NockerlTheme.accent) so every platform carries the full semantic vocabulary even where the value aliases.
  • Stacking + position: the agreed policy is max 3 visible, overflow collapses to “+N”, FIFO. The Voice HUD may keep its one-at-a-time morphing presentation (Law 9) but adopts the shared stack policy where it stacks.
  • Duration scale (settled): durations = an enum short / base / long / persistent backed by ms tokens. Voice’s literal seconds and Compose’s Material SHORT/LONG map onto the shared enum.