Why I Built This
Not because the world needed another toast library. Because the existing ones made me unreasonably angry.
Sonner's good enough problem
Emil Kowalski built something great. Sonner has 12,000 stars, millions of weekly downloads, and a spot in shadcn's official component list. It works out of the box.
“Works out of the box” becomes “impossible to change” when you actually try to customise it. Override a default style? Need !important. Use it with Tailwind v4? Broken. Avoid memory leaks in a long-running app? Good luck — dismissed toasts are never evicted from the internal array. The DOM nodes leak too.
Emil's busy. PRs sit. Issues pile up. 60+ open at last count. The codebase relies on a singleton Observer class with setTimeout + flushSync workarounds documented as “temp solutions.” Not a dig — it's what happens when a side project gets 12,000 stars and real life doesn't pause.
The issues that convinced me
These are all real Sonner GitHub issues. All still open.
Base UI changed the equation
In late 2025, the team behind Radix, Floating UI, and MUI shipped @base-ui/react v1.0 with proper toast primitives. Not a styling library. Not a component kit. Headless primitives that do the hard part and get out of your way.
createToastManager() — a singleton with a reactive store, proper cleanup on close, Floating UI positioning built in, ARIA live regions, F6 keyboard navigation. The foundation was there. Someone just needed to wrap it in a developer-friendly API.
What I built
A thin layer on top of Base UI Toast. If you know Sonner, you know Popser. Same API, different engine.
The numbers
Less than half the size. And the CSS is a separate opt-in file you control.
| Popser | Sonner | |
|---|---|---|
| JS (gzipped) | 5.6 KB | ~13.5 KB |
| CSS (gzipped) | 2.7 KB | Included in JS import |
| Total | 8.3 KB | ~17 KB |
| Memory leaks | None | #729, #605 |
| !important required | Never | For any override |
| Open issues | Few | 60+ |
| Anchored toasts | Full Floating UI | None |
| update() API | Partial updates | Re-call with same ID |
| Close button modes | always / hover / never | On or off |
Migration
95% of call sites need zero changes. The remaining 5% is mechanical rename.
- import { toast } from "sonner";
+ import { toast } from "@vcui/popser";
// toast.dismiss() still works (it's an alias for toast.close)
- toast("msg", { duration: 3000 });
+ toast("msg", { timeout: 3000 });
// duration also works as an aliasWhat this isn't
This isn't a crusade against Sonner. Emil built a great library. It's popular for good reasons. But software ages, priorities shift, and sometimes you build the thing that exists in the gap between “good enough” and “actually good.”
I needed toast notifications built on proper primitives, with clean CSS architecture, zero memory leaks, and anchored positioning. So I built it.