# A toast that stacks · Components

<!-- https://learn-ui.com/chapters/components/toast -->

In 2023 Emil Kowalski built a toast library and told the story in [Building a Toast Component](https://emilkowal.ski/ui/building-a-toast-component). **Sonner** (Sonner is an opinionated toast component for React, built by Emil Kowalski in 2023 and downloaded over 40,000,000 times per week from npm.) is downloaded over 40,000,000 times a week from npm and used by Cursor, X, and OpenAI. The market was crowded; his answer to why people chose it is a list of small decisions. The first was the name: react-toast and its kind felt cheap, so he chose the French sonner, “to ring,” trading discoverability for elegance.

### Transitions, not keyframes

The second was the animation: “I believe Sonner took off immediately because of the stacking animation which was done by some companies before, but never open sourced.” The launch video he chose focused on that motion.

His first version used CSS keyframes, and they broke when toasts arrived quickly: older toasts jumped into position because keyframes are not **interruptible** (CSS transitions can be interrupted and retargeted, even before the first transition has finished, which is what lets a toast change its destination mid-flight; keyframes cannot do this.). Transitions retarget mid-flight, so he switched to one rule, transition: transform 400ms ease. A transition needs a start state, so a useEffect sets a mounted flag after first render and writes it to a data attribute; he notes **@starting-style** (A CSS at-rule Emil Kowalski points to as a simpler way to declare the starting state of an enter transition, replacing the mounted data attribute trick.) would now make this much simpler.

> Figure. toast.cssEmil Kowalski, Building a Toast Component

Source

The keyframes-versus-transitions story, the data-mounted snippet, and the @starting-style remark are in the Animations section of Building a Toast Component.

### The stacking arithmetic

Every toast is position: absolute, offset by the gap times its index, and scaled down by 0.05 times its index for depth. The post’s diagram reads Y(0) scale(1), Y(-14px) scale(0.95), Y(-28px) scale(0.9), matching the [Toaster’s defaults](https://sonner.emilkowal.ski/toaster) of gap 14 and visibleToasts 3. Two custom properties drive it.

> Figure. CSSEmil Kowalski, Building a Toast Component

Toasts of different heights stick out unevenly, so every toast takes the height of the front one while stacked. The figure builds that stack: add toasts quickly and toggle Transitions/Keyframes to feel the jump Emil describes. Its swipe readouts matter next.

> 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

### Swipe with momentum

Swipe to dismiss is a pointer listener writing the drag distance into a --swipe-amount variable used by translateY. Removal is **momentum-based** (A swipe where you do not have to drag past a distance threshold to dismiss; if the swipe is fast enough the element is removed even when the distance is short.): distance over elapsed time gives a velocity, and the toast goes if the swipe passes SWIPE\_THRESHOLD or the velocity passes 0.11, “just a number that I ended up on through trial and error.” A short fast flick clears it where a long slow drag would not.

> Figure. TypeScriptEmil Kowalski, Building a Toast Component

Hover expands the stack. The expanded offset is index times gap plus the heights of the toasts before it, a reduce over a heights array written to an --offset variable; the expand prop makes that the resting state.

### Details and developer experience

State avoids React Context through the **Observer pattern** (To avoid using React's Context, Emil Kowalski manages Sonner's state via the Observer Pattern: the Toaster subscribes to an observable object, and whenever toast() is called the Toaster is notified and updates its state.): the Toaster subscribes, toast() notifies, the list renders with a map. No hooks, no providers. The toast.promise API, one promise plus copy for loading, success, and error, drew Theo’s “like React Query for toasts.” The API follows Timo’s react-hot-toast “because it’s simply very good,” and **developer experience** (Emil Kowalski's term for how easy a component is to use; if the component is not easy to use, people will give up before they even try it.) included a custom docs site with interactive examples.

> Figure. TypeScriptEmil Kowalski, Building a Toast Component

Then the big little details. A toast lasts 4 seconds unless hovered, and a useIsDocumentHidden hook watching document.hidden pauses the timer in a hidden tab. An :after pseudo-element fills the gaps between toasts so hover does not flicker.

A drag captures all future pointer events, and dragging upward is slowed with friction rather than blocked, “nicer than just stopping the toast immediately.” He borrows Paul Graham: “a thousand barely audible voices all singing in tune.” His verdict is developer experience plus looks, the real differentiator: “Beauty is generally underutilized in software so you can use it as leverage to stand out.”
