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-fgOverride 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.
- Toaster
classNames - Toaster
toastOptions.classNames - 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 */