Build · Components
A toast that stacks

A toast that stacks

In 2023 Emil Kowalski decided to build a toast library. He tells the story in Building a Toast Component, and he opens it with the fact that makes the story worth telling: is now downloaded over 40,000,000 times a week from npm and is used by Cursor, X, and OpenAI. The toast market was already crowded when he started, so the question he sets himself is why people chose his over proven alternatives. The answer is a list of decisions, most of them tiny.

The first was the name. Function-based names, he writes, feel cheap: react-toast, react-snackbar, react-notifications all struck him as boring and generic. He looked up French words related to notifications and landed on sonner, which means “to ring.” He is explicit about the trade: “While I’m sacrificing discoverability and clarity, it feels elegant to me.” A name that feels different is part of standing out.

The second was the animation, and this is the one he credits for the launch. “I believe Sonner took off immediately because of the stacking animation which was done by some companies before, but never open sourced.” Toasts sit one behind the other, and when a new one arrives the older ones slide back and shrink. He knew the motion was the pitch, so he tried several announcement videos and chose the one that focused on it. “This is what made people fall in love with this tiny component, it just felt right when you saw it animate.”

Getting that motion to feel right came down to a choice of CSS mechanism. Emil first wrote the stacking with CSS keyframes, and they broke the moment toasts arrived quickly: older toasts jumped into their new position instead of gliding. The reason is that keyframes are not . “That’s one downside of keyframes: you can’t smoothly change the end position while the animation is running.” Transitions can be interrupted and retargeted, so he used them instead, with a single rule, transition: transform 400ms ease. The enter animation needed a trick, because a transition needs a starting state to leave from. He sets a mounted flag in a useEffect after first render and writes it to a data attribute, so the toast starts at translateY(100%) under data-mounted=“false” and transitions to translateY(0) once the attribute flips. He notes the same thing can now be done with the at-rule, which would make the implementation much simpler.

The stacking itself is arithmetic. Every toast is position: absolute, and its vertical offset is the gap between toasts multiplied by its index. To add depth, each toast is also scaled down by 0.05 times its index. The diagram in the post labels the three visible toasts Y(0) scale(1), Y(-14px) scale(0.95), and Y(-28px) scale(0.9), which matches the Toaster’s documented defaults of a gap of 14 and visibleToasts of 3. In CSS this is driven by two custom properties, --toasts-before and --lift-amount, so the transform for a non-front toast is translateY(lift times toasts-before) scale(1 minus 0.05 times toasts-before). One thing breaks the illusion: toasts of different heights, which then stick out unevenly. His fix is to give every toast the height of the front toast while in stacked mode.

The figure below builds that stack. Add a few toasts quickly and watch the older ones move back. Its Transitions/Keyframes toggle lets you feel the difference Emil describes: under Keyframes, an arrival mid-animation makes the others jump; under Transitions they retarget without a seam. The swipe readouts show the distance and the velocity of a drag, which matters for the next paragraph.

Swipe
0px
Velocity
0.00px/ms
Release
None yet
0 in stack
Adjust
Figure 1. How a toast stack works. Add several toasts quickly in both modes: keyframes cannot be retargeted, so older toasts jump, while transitions pick up from wherever they are. Change the gap and the scale step, hover to expand, and flick the front toast down to dismiss it. The gap, scale step, visible count, 4 s duration, and 0.11 px/ms velocity are Sonner's numbers; the distance threshold here is illustrative.Constants from the source

Dismissal is by swipe, which Emil says is especially useful on devices where people already swipe notifications away. The mechanism is a pointer event listener that writes the drag distance into a --swipe-amount variable used by translateY. The decision to remove a toast is : he records when the drag started, divides the absolute distance by the elapsed time to get a velocity, and removes the toast if the swipe amount passes SWIPE_THRESHOLD or the velocity passes 0.11. The code comment is candid about that number: it is “just a number that I ended up on through trial and error.” The figure’s readouts let you see a short, fast flick clear the threshold that a long, slow drag would not.

Hovering the stack expands it so every toast is readable. The expanded offset for a toast is its index times the gap plus the sum of the heights of all toasts before it, computed with a reduce over a heights array and written to an --offset variable. If you want that to be the resting state, the Toaster takes an expand prop.

The state management is the part most people never see. To avoid React Context, Emil uses the : the Toaster component subscribes to an observable, and calling toast() notifies it, after which it renders the list with a map. The consequence for the person using it is that there are no hooks and no providers, just import toast and call it from anywhere. The toast.promise API, where you pass a promise and the copy for loading, success, and error, is the one people praise most; Emil quotes Theo calling it “like React Query for toasts.” He is also clear about lineage: the API is inspired by Timo’s react-hot-toast, “because it’s simply very good.” also meant a fully custom documentation site with interactive examples, so people could touch the product before installing it.

Then come what he calls the big little details. A toast disappears after 4 seconds by default, unless you hover it. But if the user switches tabs, those seconds pass unseen, so a useIsDocumentHidden hook watches document.hidden through the visibilitychange event and pauses the timer. The hover state depends on being over a toast, yet the gaps between toasts belong to no toast, so an :after pseudo-element fills them and keeps the hover from flickering. Once a drag begins, the toast captures all future pointer events so the drag continues even if your thumb leaves it. And dragging upward is not blocked; it is slowed with friction until it stops, because “it’s nicer than just stopping the toast immediately.” He borrows a line from Paul Graham to sum up the effect: “a thousand barely audible voices all singing in tune.” The less users notice, the better, because it means the experience is intuitive.

His own verdict on why it worked has two parts, and both are worth carrying into whatever you build next. One is developer experience: no hooks, no context, one Toaster and a function call. Two is that it looks good, with nice defaults and good animations, and he calls this the real differentiator. “Beauty is generally underutilized in software so you can use it as leverage to stand out.” Neither of those is a feature you can list. They are the sum of the decisions above, and a reader of this book can make every one of them.