implement
Toast

Toast

A stack of transient notifications in the corner of the screen.

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",
		),
	);
}

Installation

npx jsrepo add @implementjs/ui/toast

jsrepo pulls button along with it, and installs @implementjs/lucide.

Usage

The whole stack is one component. Toaster mounts the provider, the portal, the viewport, and a styled toast per entry — icon by type, title, description, and an action button when the toast carries one. Mount it once near the root of the app and push toasts from anywhere:

import { createToastManager, Toaster } from "@/lib/components/ui/toast";

export const toast = createToastManager();

// once, near the root
Toaster({ manager: toast });

// anywhere
toast.add({ title: "Saved", description: "Your changes are live.", type: "success" });

The stack

Collapsed, the toasts behind the front one peek out and scale down. Hovering fans them out by their real heights. A toast can be swiped away and keeps travelling in the direction it was thrown, and toasts past the limit wait invisibly for a slot.

All of that is one transform on Toast reading a set of CSS variables the primitive maintains, so the states swap the variables rather than fighting over the property.

Actions

An action button comes from the toast's data:

toast.add({
	title: "Message archived",
	data: { action: { label: "Undo", onClick: restore } },
});

That shape is ToasterToastData — the ready-made Toaster's own convention, not the primitive's. data is free-form, so change the shape and change Toaster to match.

Building your own

Toaster is a starting point, not a wall. The parts it assembles — ToastProvider, ToastPortal, ToastViewport, Toast, ToastTitle, ToastDescription, ToastAction, ToastClose — are all exported, so a different layout is a rewrite of one function in a file you already own.

API Reference

Every prop the styling does not consume is forwarded to the Toast primitive, so the tables below are the whole surface — the behavior props and the styling ones together.

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. Styled as a fixed 360px column in the bottom right corner. 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. Styled as the stack itself: collapsed toasts peek out behind the front one and scale down, an expanded stack fans out by real heights, and a swiped toast keeps travelling in the direction it was thrown. 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. Styled as an extra-small outline button, pushed to the right. 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. Styled as a ghost icon button in the corner; the X and its screen-reader label are built in. 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.

Toaster

The whole stack, ready made: provider, portal, viewport, and a styled toast for every entry in the manager — icon by type, title, description, and an action button when the toast carries one. Mount it once near the root and call manager.add(...) from anywhere.

PropTypeDefaultDescription
manager*ToastManagerThe manager from createToastManager() whose toasts it renders.