Skip to content

Toast queue

Toasts auto-dismiss after a delay, but the user can also close them early. The natural shape: each toast owns a scoped lifetime. Disposing the scope cancels its timer — no manual clearTimeout, no leaked callbacks.

The recipe

import { signal, effectScope } from "elements-kit/signals";
import { createTimeout } from "elements-kit/utilities/timeout";
type Toast = { id: number; message: string; dispose: () => void };
const toasts = signal<Toast[]>([]);
let nextId = 1;
function dismiss(id: number) {
const t = toasts().find((x) => x.id === id);
t?.dispose(); // cancels its timer
toasts(toasts().filter((x) => x.id !== id));
}
function show(message: string, ms = 3000) {
const id = nextId++;
const stop = effectScope(() => {
createTimeout(() => dismiss(id), ms);
});
toasts([...toasts(), { id, message, dispose: stop }]);
}

effectScope returns a stop function. createTimeout registers onCleanup against the current scope, so stop() clears the timer. The toast object carries dispose — the close button calls it directly.

How each piece contributes

PrimitiveRole
signal<Toast[]>The queue. Replaced ([…toasts(), new]) on push so subscribers re-render.
effectScope(fn)Per-toast lifetime. Owns any onCleanup registered inside fn.
createTimeout(cb, ms)The auto-dismiss. Registers onCleanup(() => clearTimeout(id)) on the surrounding scope — the very thing effectScope gives us.
For each={toasts} by={(t) => t.id}Keyed reconciliation so closing the third toast doesn’t recreate the first two.

Variations

Pause on hover — read createHover inside the timer scope. Reset the timeout when the pointer leaves:

const stop = effectScope(() => {
const hovered = createHover(host);
const t = createTimeout(() => dismiss(id), ms);
effect(() => { if (hovered()) t.stop(); else t.start(); });
});

Cap the queue — drop the oldest when over the limit:

function show(message: string) {
// …create toast…
const next = [...toasts(), toast];
if (next.length > 5) next.shift()!.dispose();
toasts(next);
}

Position variants — bind a signal<"top-right" | "bottom-center"> and switch the container’s CSS class.

Gotchas

See also

  • UtilitiescreateTimeout, createHover.
  • ScopeseffectScope lifetime semantics.
  • ListsFor keyed reconciliation.