Popser

Styling

CSS variables, classNames, Tailwind, unstyled mode. No !important. Ever.

Popser ships with default styles that work out of the box. Import them. Override what you want. Or bin the lot and start from scratch. Zero !important anywhere. Your specificity wins.

Default styles

import "@vcui/popser/styles";

Layout, animations, light/dark tokens, spinner. One import.

Just want the colour tokens without the layout opinions?

import "@vcui/popser/tokens";

CSS custom properties

Everything visual runs through --popser-* variables. Override them on :root, a wrapper div, inline styles — wherever your design system lives.

Core tokens

:root {
  --popser-bg: oklch(1 0 0);
  --popser-fg: oklch(0.145 0 0);
  --popser-border: oklch(0.922 0 0);
  --popser-radius: 8px;
  --popser-shadow: 0 4px 12px oklch(0 0 0 / 0.1);
  --popser-offset: 16px;
  --popser-gap: 8px;
  --popser-width: 356px; /* not shipped in tokens — CSS fallback default */
  --popser-stack-peek: 14px;
  --popser-hover-bg: oklch(0 0 0 / 0.05);
}

Dark mode

Activates via [data-theme="dark"] or .dark on any ancestor. No JavaScript. Just CSS doing CSS things.

[data-theme="dark"],
.dark {
  --popser-bg: oklch(0.205 0 0);
  --popser-fg: oklch(0.96 0 0);
  --popser-border: oklch(0.3 0 0);
  --popser-shadow: 0 4px 12px oklch(0 0 0 / 0.4);
  --popser-hover-bg: oklch(1 0 0 / 0.1);
}

Or just tell the Toaster:

<Toaster theme="dark" />

Rich colours

When richColors is on, each type gets its own background, border, and text.

--popser-success-bg     --popser-success-border     --popser-success-fg
--popser-error-bg       --popser-error-border       --popser-error-fg
--popser-info-bg        --popser-info-border        --popser-info-fg
--popser-warning-bg     --popser-warning-border     --popser-warning-fg
--popser-loading-bg     --popser-loading-border     --popser-loading-fg

Override any of them. Make success pink. Make errors chartreuse. No judgement.

Per-toast styles

Inline styles, custom borders, monospace — override anything on a single toast.

toast.success("Saved", {
  style: { borderLeft: "4px solid #22c55e" },
});

toast("Minimal", {
  icon: false,
  style: { fontFamily: "monospace", fontSize: 13 },
});

ClassNames

Target every slot. Works with Tailwind, CSS modules, or whatever class-based religion you follow.

Global (Toaster level)

<Toaster
  classNames={{
    viewport: "...",
    root: "...",
    content: "...",
    header: "...",
    title: "...",
    description: "...",
    icon: "...",
    actions: "...",
    actionButton: "...",
    cancelButton: "...",
    closeButton: "...",
    arrow: "...",
  }}
/>

Per-toast

toast.success("Saved", {
  className: "border-green-500",
  classNames: {
    title: "font-bold",
    description: "text-sm",
  },
});

Merge order

Three levels. Nothing replaced. Everything concatenated.

  1. Toaster classNames
  2. Toaster toastOptions.classNames
  3. Per-toast classNames / className

Tailwind

Works out of the box. No special config. Just pass classes.

<Toaster
  classNames={{
    root: "rounded-lg border shadow-lg",
    title: "font-semibold text-sm",
    description: "text-muted-foreground text-xs",
    actionButton: "bg-primary text-primary-foreground",
    cancelButton: "border border-input",
  }}
/>

Per-toast:

toast.success("Saved", { className: "border-green-500" });

Unstyled mode

Nuclear option. Strip everything. Start from zero.

<Toaster unstyled />

Or per-toast:

toast("Raw", { unstyled: true });

Target elements with data-popser-* attributes:

[data-popser-viewport] { }
[data-popser-root] { }
[data-popser-content] { }
[data-popser-header] { }
[data-popser-title] { }
[data-popser-description] { }
[data-popser-icon] { }
[data-popser-actions] { }
[data-popser-action] { }
[data-popser-cancel] { }
[data-popser-close] { }
[data-popser-arrow] { }

Data attributes

Viewport

[data-popser-viewport] { }
[data-popser-viewport][data-position="top-center"] { }
[data-popser-viewport][data-theme="dark"] { }
[data-popser-viewport][data-rich-colors] { }
[data-popser-viewport][data-expanded] { }
[data-popser-viewport][data-mobile] { }
[data-popser-viewport][data-dir="rtl"] { }

Toast root

[data-popser-root] { }
[data-popser-root][data-type="success"] { }
[data-popser-root][data-type="error"] { }
[data-popser-root][data-type="custom"] { }
[data-popser-root][data-rich-colors] { }
[data-popser-root][data-unstyled] { }
[data-popser-root][data-anchored] { }
[data-popser-root][data-enter-from="left"] { }
[data-popser-root][data-popser-id="my-id"] { }

Close button

[data-popser-close][data-close-button="hover"] { }
[data-popser-close][data-close-button="always"] { }
[data-popser-close][data-close-position="header"] { }
[data-popser-close][data-close-position="corner"] { }

Arrow

[data-popser-arrow][data-side="top"] { }
[data-popser-arrow][data-side="bottom"] { }

Animation duration

Override the default transition timing with CSS custom properties. No JS changes needed.

:root {
  --popser-transition-duration: 400ms;            /* default for all toasts */
  --popser-anchored-transition-duration: 200ms;   /* default for anchored toasts */
}

Want snappier transitions? Slower exits? Just change the variable.

[data-popser-viewport] {
  --popser-transition-duration: 250ms;
}

Works on any ancestor — :root, viewport, or a wrapper div.

Entry direction

Override the default entry/exit animation direction per toast with enterFrom. Defaults to position-based (bottom toasts slide up, top toasts slide down).

toast("Sliding in from the left", { enterFrom: "left" });
toast("Coming from above", { enterFrom: "top" });

Sets data-enter-from on the toast root. CSS handles the rest. Options: "top", "bottom", "left", "right".

Animations

Default styles include enter/exit animations:

  • Enter: fade in + scale from 0.95 + slide from edge
  • Exit: fade out + slide toward edge
  • Swipe: slide in swipe direction
  • Reduced motion: disabled via prefers-reduced-motion: reduce

Direction depends on position — bottom slides up, top slides down. We thought about it.

Uses data-starting-style and data-ending-style from Base UI for CSS-only transitions.

Runtime CSS variables

Computed by popser. Available for your CSS to do clever things with:

--toast-index              /* 0 = newest, 1 = next, etc. */
--toast-offset-y           /* cumulative height offset (px) */
--toast-height             /* measured height of this toast (px) */
--toast-frontmost-height   /* height of the front toast (px) */
--popser-visible-count     /* number of visible toasts */

On this page