Toast API
Call it anywhere. No hooks. No providers. Just rectangles appearing on screen.
Import toast. Call it anywhere — components, event handlers, server actions, inside a setTimeout at 3am. No hooks. No context. Just function calls that summon rectangles.
Every method spits back a toast ID. Keep it if you care. Bin it if you don't.
import { toast } from "@vcui/popser";Types
Six flavours. Each one gets its own icon and colours.
toast("Hello world"); // default — no icon, no drama
toast.success("Saved"); // green check
toast.error("Something broke"); // red circle
toast.info("Heads up"); // blue info circle
toast.warning("Careful"); // yellow triangle
toast.loading("Uploading..."); // spinner, same timeout as otherstoast.message() is an alias for toast(). Exists because Sonner has it. Compatibility is a hell of a drug.
Description
Secondary text. Below the title. Pass it in options.
toast("With description", { description: "More detail here" });Loading
Uses the same default timeout as every other toast. If you want it to stick around until you deal with it, set timeout: 0. Then close it or update it. Those are your options.
const id = toast.loading("Uploading...", { timeout: 0 });
// later, when you can be bothered
toast.close(id);Update
Mutate a living toast. Title, type, description, timeout — change whatever you want. The toast doesn't care about your indecisiveness.
const id = toast.loading("Uploading...");
toast.update(id, {
title: "Upload complete",
description: "file.pdf",
type: "success",
timeout: 3000,
});Close
Pass an ID to kill one. Pass nothing to kill them all.
toast.close("my-id"); // surgical
toast.close(); // scorched earthtoast.dismiss() does the same thing. Sonner compatibility. Different name, same body count.
Actions
Toasts can have buttons. Pass a PopserAction object if you want clicks, or raw ReactNode if you just want it to sit there looking pretty.
toast.success("File uploaded", {
action: {
label: "View",
onClick: () => router.push("/files"),
},
cancel: {
label: "Undo",
onClick: () => undoUpload(),
},
});
// ReactNode — decorative, no click handler
toast.info("Tip", { action: <span>Learn more →</span> });PopserAction shape:
{
label: ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}Custom
Full anarchy. Bypass the default layout. Render whatever JSX you want.
toast.custom((id) => (
<div className="my-toast">
<p>Custom content</p>
<button onClick={() => toast.close(id)}>Close</button>
</div>
));You get the toast id. You're responsible for everything else. More carnage in Custom Toasts.
Promise
The state machine you were too busy to build. Whole page dedicated to it: Promise Toasts.
Deduplication
deduplicate: true tracks active toasts by title. Same title already showing? You get the existing ID back. One toast on screen. No spam.
const id1 = toast.error("Connection lost", { deduplicate: true });
const id2 = toast.error("Connection lost", { deduplicate: true });
// id1 === id2 — one toast, not twoDismiss the original and the slot opens up. Only works with string titles — ReactNode titles are too chaotic to compare.
Get active toasts
For when you need to know how much damage you've done.
const ids = toast.getToasts(); // ["abc", "def"]Toast history
Opt-in audit trail. Enable with historyLength on <Toaster>, then query whenever you fancy.
const history = toast.getHistory();
// [{ id, title, type, createdAt, closedAt, closedBy }, ...]
toast.clearHistory(); // wipe the lotEach entry tracks closedBy: "auto" (timeout), "manual" (user/code), or "limit" (evicted by stack limit). Disabled by default — set historyLength to enable. See Toaster for setup.
Options reference
Every toast method takes these as a second argument. Mix and match.
toast.success("Saved", {
description: "Your changes are live",
timeout: 3000,
icon: <CheckIcon />,
});Content
| Option | Type | Default | Description |
|---|---|---|---|
description | ReactNode | — | Secondary text below the title |
icon | ReactNode | false | Auto per type | Custom icon, or false to hide |
type | PopserType | Method-dependent | "success" | "error" | "info" | "warning" | "loading" |
Behaviour
| Option | Type | Default | Description |
|---|---|---|---|
id | string | Auto-generated | Custom toast ID for updates and targeting |
timeout | number | 4000 | Auto-dismiss in ms. 0 = it lives forever |
duration | number | — | Alias for timeout (Sonner compat) |
deduplicate | boolean | false | Block duplicates with same string title |
dismissible | boolean | true | Allow swipe/close button dismissal |
enterFrom | "top" | "bottom" | "left" | "right" | Position-based | Override entry/exit animation direction |
closeButtonPosition | "header" | "corner" | "header" | Where to render the close button for this toast |
priority | "low" | "high" | — | ARIA live region priority |
data | Record<string, unknown> | — | Your custom data, along for the ride |
Actions
| Option | Type | Description |
|---|---|---|
action | PopserAction | ReactNode | Primary action button |
cancel | PopserAction | ReactNode | Cancel button (also closes the toast) |
Anchoring
| Option | Type | Default | Description |
|---|---|---|---|
anchor | Element | MouseEvent | {x, y} | null | — | Anchor element or coordinates |
anchorSide | "top" | "bottom" | "left" | "right" | "inline-end" | "inline-start" | "bottom" | Which side of the anchor |
anchorAlign | "start" | "center" | "end" | "center" | Alignment along the anchor side |
anchorOffset | number | 8 | Distance from anchor in px |
anchorAlignOffset | number | 0 | Offset along alignment axis in px |
anchorPositionMethod | "absolute" | "fixed" | "absolute" | CSS positioning method |
anchorSticky | boolean | false | Stay visible when anchor scrolls off |
anchorCollisionBoundary | "clipping-ancestors" | Element | Element[] | "clipping-ancestors" | Collision boundary |
anchorCollisionPadding | number | 5 | Collision padding in px |
arrow | boolean | false | Show arrow pointing at anchor |
arrowPadding | number | 5 | Arrow edge padding in px |
More than you'll ever need. Full guide: Anchored Toasts.
Styling (per-toast)
| Option | Type | Description |
|---|---|---|
className | string | Class on the toast root |
classNames | Partial<PopserClassNames> | Per-slot class names |
style | CSSProperties | Inline styles on the toast root |
richColors | boolean | Coloured background for this toast |
unstyled | boolean | Strip default styles for this toast |
Callbacks
All callbacks get the toast id. Except onRemove — by that point the toast is dead and gone.
| Callback | When it fires |
|---|---|
onClose(id) | Toast starts closing (always fires) |
onAutoClose(id) | Auto-dismissed by timeout |
onDismiss(id) | Manually dismissed (swipe, close button, toast.close(id)) |
onRemove() | Removed from DOM after exit animation |
Firing order:
Manual dismiss: onDismiss → onClose → (animation) → onRemove
Auto dismiss: onAutoClose → onClose → (animation) → onRemove
TypeScript
Everything's exported. Go wild.
import type {
PopserOptions,
PopserUpdateOptions,
PopserAction,
PopserType,
PopserPosition,
PopserClassNames,
PopserIcons,
PopserSwipeDirection,
PopserPromiseOptions,
PopserPromiseExtendedResult,
ToasterProps,
ToastHistoryEntry,
} from "@vcui/popser";