The <Toaster> component renders the toast viewport. Drop it once in your app. Anywhere in the tree is fine — it portals to document.body.
import { Toaster } from "@vcui/popser";
function App() {
return (
<>
<YourApp />
<Toaster />
</>
);
}No Provider wrapper needed. The Toaster handles Provider, Portal, and Viewport internally.
| Prop | Type | Default | Description |
|---|---|---|---|
position |
PopserPosition |
"bottom-right" |
Where toasts appear |
limit |
number |
3 |
Max visible toasts at once |
offset |
number | string |
16 |
Distance from viewport edge (px or CSS value) |
mobileOffset |
number | string |
Falls back to offset |
Distance from edge on mobile |
gap |
number |
8 |
Space between toasts when expanded (px) |
mobileBreakpoint |
number |
600 |
Width in px that triggers mobile layout |
expand |
boolean |
false |
Always show toasts in expanded column |
expandedLimit |
number |
— | Max visible toasts when hovered/expanded. Collapsed still uses limit |
dir |
"ltr" | "rtl" | "auto" |
— | Text direction. "auto" reads document.documentElement.dir |
| Prop | Type | Default | Description |
|---|---|---|---|
timeout |
number |
4000 |
Global auto-dismiss in ms |
closeButton |
"always" | "hover" | "never" |
"hover" |
Close button visibility mode |
closeButtonPosition |
"header" | "corner" |
"header" |
Close button placement (header row or top-right corner) |
swipeDirection |
SwipeDirection | SwipeDirection[] |
["down", "right"] |
Swipe-to-dismiss direction(s) |
historyLength |
number |
0 (disabled) |
Max history entries. Enables toast.getHistory() |
| Prop | Type | Default | Description |
|---|---|---|---|
theme |
"light" | "dark" | "system" |
"system" |
Color scheme |
richColors |
boolean |
false |
Colored backgrounds per toast type |
unstyled |
boolean |
false |
Strip all default styles |
icons |
PopserIcons |
Built-in SVGs | Override icons per type |
classNames |
PopserClassNames |
— | Class names for every slot |
style |
CSSProperties |
— | Inline styles on the viewport |
| Prop | Type | Default | Description |
|---|---|---|---|
toastOptions |
Partial<PopserOptions> |
— | Default options applied to every toast |
ariaLabel |
string |
— | Custom ARIA label for the viewport |
Six positions. Pick one.
top-left top-center top-right
bottom-left bottom-center bottom-right
<Toaster position="top-center" />"system" (default) detects via prefers-color-scheme media query. Or lock it:
<Toaster theme="dark" />Theme is applied as data-theme="light" or data-theme="dark" on the viewport element. If you're using next-themes or any system that sets .dark on an ancestor, popser picks that up automatically via CSS.
Below the mobileBreakpoint (default 600px), toasts switch to:
- Full viewport width
- Close button always visible
- Offset from
mobileOffset(falls back tooffset) data-mobileattribute on the viewport
No hardcoded media queries in your code. Just a prop.
<Toaster mobileBreakpoint={768} mobileOffset="24px" />By default, toasts collapse into a stack with a peek effect. Hover to expand. Set expand to always show the full list:
<Toaster expand />Stack state is exposed via data-expanded on the viewport. The collapse has a 100ms debounce on mouse leave to prevent flicker.
<Toaster closeButton="always" /> {/* Always visible */}
<Toaster closeButton="hover" /> {/* Shown on toast hover (default) */}
<Toaster closeButton="never" /> {/* No close button */}Override any or all icons globally:
<Toaster
icons={{
success: <MyCheckIcon />,
error: <MyXIcon />,
info: <MyInfoIcon />,
warning: <MyAlertIcon />,
loading: <MySpinner />,
close: <MyCloseIcon />,
}}
/>Built-in icons are inline SVGs with currentColor — they inherit your text color automatically.
Apply options to every toast via toastOptions:
<Toaster
toastOptions={{
timeout: 8000,
richColors: true,
classNames: {
root: "my-toast",
title: "my-toast-title",
},
}}
/>Per-toast options override toastOptions. Per-toast overrides Toaster. Always.
Target every slot globally:
<Toaster
classNames={{
viewport: "my-viewport",
root: "my-toast",
content: "my-content",
header: "my-header",
title: "my-title",
description: "my-description",
icon: "my-icon",
actions: "my-actions",
actionButton: "my-action",
cancelButton: "my-cancel",
closeButton: "my-close",
arrow: "my-arrow",
}}
/>Class names are merged across three levels:
- Toaster
classNames toastOptions.classNames- Per-toast
classNames
All three concatenate. Nothing gets replaced.
Control which directions dismiss a toast:
<Toaster swipeDirection="right" />
<Toaster swipeDirection={["down", "left"]} />Options: "up", "down", "left", "right". Accepts a single direction or an array.
By default, hovering over a collapsed stack shows all toasts up to limit. Use expandedLimit to show more toasts when expanded/hovered:
<Toaster limit={3} expandedLimit={8} />Collapsed: 3 visible. Hover: up to 8. Useful when you want a clean default view but let users see more on demand.
Control where the close button renders:
<Toaster closeButtonPosition="corner" /> {/* Top-right corner of the toast */}
<Toaster closeButtonPosition="header" /> {/* In the header row (default) */}Per-toast override:
toast.success("Saved", { closeButtonPosition: "corner" });Resolution order: per-toast > Toaster > default ("header").
<Toaster dir="rtl" />
<Toaster dir="auto" /> {/* Reads document.documentElement.dir */}When dir is "rtl":
- Positions flip:
leftbecomesrightand vice versa - Swipe directions adjust accordingly
- Enter/exit animations mirror
"auto" detects the direction from document.documentElement.dir at mount time.
Enable history tracking by setting historyLength:
<Toaster historyLength={50} />Then use the imperative API:
const history = toast.getHistory();
// [{ id, title, type, createdAt, closedAt, closedBy }]
toast.clearHistory();Set to 0 or omit to disable. History entries are capped at historyLength — oldest entries are evicted first.
The Toaster includes a built-in error boundary. If one toast throws during render, the rest keep working. Errors are logged to console. Your app doesn't crash because someone passed a bad icon.