Popser

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 others

toast.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 earth

toast.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 two

Dismiss 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 lot

Each 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

OptionTypeDefaultDescription
descriptionReactNode—Secondary text below the title
iconReactNode | falseAuto per typeCustom icon, or false to hide
typePopserTypeMethod-dependent"success" | "error" | "info" | "warning" | "loading"

Behaviour

OptionTypeDefaultDescription
idstringAuto-generatedCustom toast ID for updates and targeting
timeoutnumber4000Auto-dismiss in ms. 0 = it lives forever
durationnumber—Alias for timeout (Sonner compat)
deduplicatebooleanfalseBlock duplicates with same string title
dismissiblebooleantrueAllow swipe/close button dismissal
enterFrom"top" | "bottom" | "left" | "right"Position-basedOverride entry/exit animation direction
closeButtonPosition"header" | "corner""header"Where to render the close button for this toast
priority"low" | "high"—ARIA live region priority
dataRecord<string, unknown>—Your custom data, along for the ride

Actions

OptionTypeDescription
actionPopserAction | ReactNodePrimary action button
cancelPopserAction | ReactNodeCancel button (also closes the toast)

Anchoring

OptionTypeDefaultDescription
anchorElement | 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
anchorOffsetnumber8Distance from anchor in px
anchorAlignOffsetnumber0Offset along alignment axis in px
anchorPositionMethod"absolute" | "fixed""absolute"CSS positioning method
anchorStickybooleanfalseStay visible when anchor scrolls off
anchorCollisionBoundary"clipping-ancestors" | Element | Element[]"clipping-ancestors"Collision boundary
anchorCollisionPaddingnumber5Collision padding in px
arrowbooleanfalseShow arrow pointing at anchor
arrowPaddingnumber5Arrow edge padding in px

More than you'll ever need. Full guide: Anchored Toasts.

Styling (per-toast)

OptionTypeDescription
classNamestringClass on the toast root
classNamesPartial<PopserClassNames>Per-slot class names
styleCSSPropertiesInline styles on the toast root
richColorsbooleanColoured background for this toast
unstyledbooleanStrip default styles for this toast

Callbacks

All callbacks get the toast id. Except onRemove — by that point the toast is dead and gone.

CallbackWhen 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";

On this page