Toaster
One component. Drop it once. Forget about it.
<Toaster> renders the viewport. Drop it once, anywhere in the tree. It portals to document.body so your carefully crafted layout remains unbothered.
import { Toaster } from "@vcui/popser";
function App() {
return (
<>
<YourApp />
<Toaster />
</>
);
}No Provider. No context. Toaster handles all that internally because you've got better things to do.
Props
Layout
| Prop | Type | Default | Description |
|---|---|---|---|
position | PopserPosition | "bottom-right" | Where toasts show up |
limit | number | 3 | Max visible toasts (collapsed) |
expandedLimit | number | Falls back to limit | Max visible toasts when expanded (hovered) |
offset | number | string | 16 | Distance from viewport edge |
mobileOffset | number | string | Falls back to offset | Distance from edge on mobile |
gap | number | 8 | Space between expanded toasts |
mobileBreakpoint | number | 600 | Width that triggers mobile layout |
expand | boolean | false | Always show toasts expanded |
Behaviour
| Prop | Type | Default | Description |
|---|---|---|---|
timeout | number | 4000 | Global auto-dismiss in ms |
closeButton | "always" | "hover" | "never" | "hover" | Close button visibility |
closeButtonPosition | "header" | "corner" | "header" | Where to render close buttons |
swipeDirection | SwipeDirection | SwipeDirection[] | ["down", "right"] | Swipe-to-dismiss direction(s) |
Appearance
| Prop | Type | Default | Description |
|---|---|---|---|
theme | "light" | "dark" | "system" | "system" | Colour scheme |
richColors | boolean | false | Coloured backgrounds per 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 |
Advanced
| Prop | Type | Default | Description |
|---|---|---|---|
toastOptions | Partial<PopserOptions> | — | Defaults applied to every toast |
ariaLabel | string | — | Custom ARIA label for the viewport |
dir | "ltr" | "rtl" | "auto" | — | Text direction. "auto" reads from document.documentElement.dir |
historyLength | number | — | Max entries in the toast history ring buffer. 0 or unset disables |
Positions
Six spots. Pick your favourite corner. Or centre, if you enjoy blocking content. Default is highlighted.
<Toaster position="top-center" />Theme
"system" reads prefers-color-scheme. Or just tell it what you want:
<Toaster theme="dark" />Sets data-theme on the viewport. Using next-themes or anything that slaps .dark on an ancestor? Popser picks it up via CSS. No config. It just works.
Mobile
Below mobileBreakpoint (default 600px), toasts switch to:
- Full viewport width
- Close button always visible (thumbs are imprecise)
mobileOffsetfor spacingdata-mobileattribute on the viewport
No hardcoded media queries. Just a prop.
<Toaster mobileBreakpoint={768} mobileOffset="24px" />Stack
Toasts collapse into a stack by default. Hover to expand. Hate the stack? Spread everything out.
<Toaster expand />Stack state lives on data-expanded. Mouse leave has a 100ms debounce because without it, every accidental pixel causes a seizure.
Expanded limit
Show more toasts when hovering than when collapsed. limit controls the collapsed count, expandedLimit controls the expanded count.
<Toaster limit={3} expandedLimit={8} />Collapsed: 3 toasts visible. Hover: up to 8. The provider keeps enough toasts alive internally to support the higher count.
Rich colours
Coloured backgrounds per type. One prop.
<Toaster richColors />Close button
Visibility
Three modes. Choose based on how much you trust your users.
<Toaster closeButton="always" /> {/* trust no one */}
<Toaster closeButton="hover" /> {/* default — civilised */}
<Toaster closeButton="never" /> {/* no escape */}Position
Where the close button lives. "header" puts it inline in the toast header. "corner" positions it absolute in the top-right corner.
<Toaster closeButtonPosition="corner" />Per-toast override:
toast("Important", { closeButtonPosition: "corner" });Resolution: per-toast > Toaster > default "header".
RTL support
Right-to-left layout. Set dir to "rtl" or "auto" to read from document.documentElement.dir.
<Toaster dir="rtl" />When RTL is active:
- Positions flip:
"top-right"→"top-left","bottom-right"→"bottom-left"(center positions unchanged) - Swipe directions flip: left ↔ right
- Entry/exit animations mirror horizontally
data-dir="rtl"set on the viewport for CSS targeting
<Toaster dir="auto" /> {/* reads document.documentElement.dir */}Toast history
Opt-in ring buffer of recent toast events. Enable with historyLength.
<Toaster historyLength={50} />Then query from anywhere:
const history = toast.getHistory();
// [{ id, title, type, createdAt, closedAt, closedBy }, ...]
toast.clearHistory();Disabled by default (0 or unset). The buffer evicts oldest entries when over capacity. Each entry tracks closedBy: "auto", "manual", or "limit".
Custom icons
Don't like the built-in SVGs? Swap any or all. Emoji, custom components, false to kill them entirely.
<Toaster
icons={{
success: <MyCheckIcon />,
error: <MyXIcon />,
info: <MyInfoIcon />,
warning: <MyAlertIcon />,
loading: <MySpinner />,
close: <MyCloseIcon />,
}}
/>Built-in icons use currentColor so they inherit your text colour. Per-toast override via icon prop.
Default toast options
Apply options to every toast globally. Per-toast options still override, because specificity matters.
<Toaster
toastOptions={{
timeout: 8000,
richColors: true,
classNames: {
root: "my-toast",
title: "my-toast-title",
},
}}
/>ClassNames
Target every slot:
<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",
}}
/>Merges across three levels. Nothing replaced, everything concatenated:
- Toaster
classNames toastOptions.classNames- Per-toast
classNames
Swipe direction
Control which way dismisses. Default is right and down.
<Toaster swipeDirection="right" />
<Toaster swipeDirection={["down", "left"]} />Options: "up", "down", "left", "right". Single value or array.
Error boundary
Built in. One toast throws? The rest keep going. Errors hit the console. Your app doesn't crash because someone passed a malformed icon. You're welcome.