implement
Toast

Toast

Briefly announce the result of an action in a stack of self-dismissing messages.

import { Div } from "@implementjs/core";
import { Button } from "@/lib/components/ui/button";
import { createToastManager, Toaster, type ToasterToastData } from "@/lib/components/ui/toast";

const manager = createToastManager();

function saveDocument() {
	return new Promise<string>((resolve) => setTimeout(() => resolve("Quarterly report"), 2000));
}

export default function ToastDemo() {
	return Div(
		{ class: "flex flex-wrap items-center justify-center gap-2" },
		Toaster({ manager }),
		Button(
			{
				variant: "outline",
				onClick: () =>
					manager.add({
						title: "Event created",
						description: "Friday, August 21 at 4:00 PM",
					}),
			},
			"Show toast",
		),
		Button(
			{
				variant: "outline",
				onClick: () =>
					manager.add({
						type: "success",
						title: "Changes saved",
					}),
			},
			"Success",
		),
		Button(
			{
				variant: "outline",
				onClick: () =>
					manager.add({
						type: "error",
						title: "Something went wrong",
						description: "The changes could not be saved.",
					}),
			},
			"Error",
		),
		Button(
			{
				variant: "outline",
				onClick: () =>
					manager.add({
						title: "Message archived",
						data: {
							action: {
								label: "Undo",
								onClick: () => manager.add({ title: "Message restored" }),
							},
						} satisfies ToasterToastData,
					}),
			},
			"With action",
		),
		Button(
			{
				variant: "outline",
				onClick: () =>
					manager.promise(saveDocument(), {
						loading: "Saving document…",
						success: (name) => `${name} saved`,
						error: "Could not save the document",
					}),
			},
			"Promise",
		),
	);
}

A toast is a short message that appears, waits, and leaves on its own. The pieces are split the same way as Base UI's Toast: a manager owns the list and the clocks, ToastProvider shares it, ToastViewport is the region the stack lives in, and Toast renders one message with ToastTitle, ToastDescription, ToastAction, and ToastClose inside it.

import { ForEach } from "@implementjs/core";
import {
	createToastManager,
	Toast,
	ToastClose,
	ToastDescription,
	ToastPortal,
	ToastProvider,
	ToastTitle,
	ToastViewport,
} from "@implementjs/primitives";

const manager = createToastManager();

ToastProvider(
	{ manager },
	ToastPortal(
		ToastViewport(
			ForEach(
				manager.toasts,
				(t) => t.id,
				(toast) =>
					Toast(
						{ toast },
						ToastTitle(toast.bind((t) => t.title ?? "")),
						ToastDescription(toast.bind((t) => t.description ?? "")),
						ToastClose("Close"),
					),
			),
		),
	),
);

manager.add({ title: "Event created", description: "Friday at 4:00 PM" });

Unlike the other primitives, toasts are created imperatively: the tree above mounts once, and manager.add is called from wherever something worth announcing happens.

The manager

createToastManager() returns the object that owns everything: toasts is a signal holding the list (frontmost first), and add, update, close, remove, and promise change it. Create it at module scope so any code can import it and push a message; the tree renders whatever the manager holds.

const id = manager.add({
	title: "Message archived",
	description: "It moved to the Archive folder.",
	type: "success",
	timeout: 8000,
});

manager.update(id, { title: "Two messages archived" });
manager.close(id);
manager.close(); // close everything

add returns the toast's id. Passing an existing id to add updates that toast in place instead of stacking a duplicate — useful for progress that keeps replacing itself. update also restarts the toast's clock so the new content gets a full stay. close starts the exit; the toast leaves the list once its exit transition finishes (see Animation).

Everything you pass to add rides along on the toast object: type becomes data-type on every part for styling, priority: "high" makes screen readers announce it assertively, and data carries anything your render function needs — an icon, a payload, an action callback. onClose fires when the toast starts leaving, onRemove when it is gone.

Timers

Each toast dismisses itself after timeout milliseconds — its own, or the default from the provider or manager (5000). timeout: 0 keeps a toast until it is closed explicitly.

The clocks pause while the pointer is over the stack, while the stack holds keyboard focus, and while the window is blurred or the tab hidden — a toast never quietly expires while it is being read or nobody is looking. manager.pause() and manager.resume() do the same by hand.

At most limit toasts (default 3) are visible at once. Extra toasts stay in the list marked data-limited — hide them in CSS — and their clocks hold until they surface.

Promises

manager.promise covers the common async flow: it shows a loading toast (with type: "loading" and no timeout), then updates it into the success or error state when the promise settles. Each state takes a string, an options object, or a function of the resolved value:

manager.promise(saveDocument(), {
	loading: "Saving…",
	success: (doc) => `${doc.name} saved`,
	error: { title: "Could not save", description: "Check your connection." },
});

The promise is returned unchanged, so errors still reach your own handling.

The viewport and stacking

ToastViewport is a role="region" landmark. Position it yourself (fixed bottom-6 right-6 in the demo) — the primitive does not choose a corner. Pressing F6 (or the provider's hotkey) moves focus into it so keyboard users can reach the stack; each toast is focusable, and Escape on one dismisses it.

Hovering or focusing the viewport sets data-expanded on the viewport and every toast. Each Toast measures itself and exposes the stacking math as CSS variables, so collapsed and expanded layouts are pure CSS:

  • --toast-index — position from the front (0 is frontmost)
  • --toast-offset-y — distance in px to this toast's expanded slot, from real heights plus the provider's gap
  • --toast-height and --toast-frontmost-height — measured heights

data-behind marks every toast that is not frontmost.

Swipe to dismiss

Toasts can be flicked away with a pointer. swipeDirection on Toast picks the allowed direction(s) — the default ["down", "right"] suits a bottom-right stack. While a drag is in flight the root has data-swiping and writes --toast-swipe-movement-x / --toast-swipe-movement-y; feed those into the transform so the toast follows the pointer 1:1 (and disable transitions under data-swiping). Past 45px the toast dismisses with data-swipe-direction left on it for a directional exit; short of that it springs back. Buttons, links, inputs, and anything under [data-swipe-ignore] never start a swipe.

Animation

A toast's element mounts when it is added, so entrances use @starting-style (the starting: variant), exactly like a freshly-mounted dialog. Exits are the reverse trick: close flips data-state to "closed" but keeps the toast in the list while your closing styles transition, removing it only after the longest transition or animation on the root finishes. No transition means instant removal.

Toast({
	toast,
	class:
		"transition-[transform,opacity] duration-300 " +
		"starting:data-[state=open]:opacity-0 data-[state=closed]:opacity-0",
});

The demo drives its whole stack from one transform that reads the CSS variables above, so entering, stacking, expanding, swiping, and leaving all share a single declaration — the states only swap variable values. Open the demo's source to see the full set of classes.

API Reference

createToastManager

Creates the ToastManager that owns the toast list and the clocks. toasts is a signal holding the list frontmost-first; add, update, close, remove, promise, pause, and resume change it. Usually created at module scope so any code can push a message.

PropTypeDefaultDescription
timeoutnumber5000Auto-dismiss delay in ms for toasts that don't set their own. 0 disables.
limitnumber3How many toasts show at once. Extra toasts stay in the list with data-limited and their clocks held.

ToastProvider

Provides the manager and timing to every toast part inside it. Pauses every clock while the pointer is over the stack, while it holds focus, and while the window is blurred or the tab hidden. Makes its own manager when none is passed.

PropTypeDefaultDescription
managerToastManagerA manager from createToastManager(). Omitted, the provider creates a private one.
timeoutnumber5000Overrides the manager's default auto-dismiss delay.
limitnumber3Overrides the manager's visible-toast limit.
gapnumber16Pixels between expanded toasts, used when computing --toast-offset-y.
hotkeystring"F6"The key that moves focus into the viewport.

ToastViewport

The landmark region holding the stack. Sets role="region" with an aria-label naming the hotkey. Position it yourself; render the toasts inside it with ForEach over manager.toasts. Hover or focus expands the stack and pauses the clocks. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-toast-viewport]Present
[data-expanded]Present while hovered or focused

Toast

One toast. Sets role="status" with aria-live from the toast's priority, handles swipe-to-dismiss and Escape, and removes itself after the exit transition (data-state="closed") finishes. Focusable. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
toast*Readable<ToastData>The toast to render — the readable ForEach hands the render function.
swipeDirectionSwipeDirection | SwipeDirection[]["down", "right"]Which swipe direction(s) dismiss the toast.
Data attributeValue
[data-toast-root]Present
[data-state]"open" | "closed"
[data-type]The toast's type, when set
[data-expanded]Present while the stack is expanded
[data-behind]Present when not the frontmost toast
[data-limited]Present when past the visible limit
[data-swiping]Present while a swipe is in flight
[data-swipe-direction]"up" | "down" | "left" | "right" while swiping and through the exit
CSS variableDescription
--toast-indexPosition from the front; 0 is the frontmost toast.
--toast-offset-yDistance in px to this toast's expanded slot, from measured heights plus the provider's gap.
--toast-heightThis toast's measured height in px.
--toast-frontmost-heightThe frontmost toast's measured height in px, for clamping a collapsed stack.
--toast-swipe-movement-xHorizontal pointer travel in px during a swipe.
--toast-swipe-movement-yVertical pointer travel in px during a swipe.

ToastTitle

The toast's heading. The root points aria-labelledby at it. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-toast-title]Present
[data-type]The toast's type, when set

ToastDescription

Supporting copy. The root points aria-describedby at it. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-toast-description]Present
[data-type]The toast's type, when set

ToastAction

A button for the toast's action (undo, retry, …). Runs your onClick, then closes the toast. Renders a Button; extra props are forwarded onto it.

Data attributeValue
[data-toast-action]Present
[data-type]The toast's type, when set

ToastClose

Dismisses its toast. Labelled for assistive technology by default. Renders a Button; extra props are forwarded onto it.

Data attributeValue
[data-toast-close]Present
[data-type]The toast's type, when set

ToastPortal

Renders its children into another DOM parent so the stack escapes overflow and stacking contexts. This is the core Portal helper; context still resolves from where the portal is declared.

PropTypeDefaultDescription
toHTMLElement | Ref<HTMLElement>document.bodyThe parent to teleport into.
disabledSignal<boolean> | booleanfalseMounts the children in place instead of teleporting.