4 April 2026
We replaced our toast library with 230 lines. The strings were the hard part.
Sonner is a good toast library. It slides in, stacks neatly, auto-dismisses. For most apps, it’s the right choice. For ours, it wasn’t – because every time the user tapped Save three times in a row, they got three identical toasts climbing the screen like a notification anxiety dream.
We replaced it with 230 lines of React and Framer Motion. A capsule toast system where duplicate toasts merge instead of stacking, where loading toasts morph into success toasts in place, and where you can flick a toast away by dragging upward. The animation took a day. The copy took a week.
Toasts shouldn’t stack. They should merge.
The core insight is simple: if the same message appears twice, it’s the same event happening again, not a new event. The user doesn’t need two capsules – they need one capsule that acknowledges it counted twice. A count badge and a subtle scale pulse (1 to 1.08 to 1, 250ms, ease-out) tells the user their tap registered without adding visual noise.
The merge logic checks variant + title. If a toast with the same combination already exists, it bumps the count and resets the auto-dismiss timer. The pulse is triggered by a bump counter in the entry, not by the count itself – so the animation fires on every merge, not just when the number changes.
The capsule, not the rectangle
Most toast libraries render rectangles with rounded corners. We use a pill shape (rounded-full) for simple toasts and rounded-2xl when there’s a description. The shape is the signal: a pill is ephemeral, transient, passing through. A rectangle is a card – it implies permanence, demands reading. The pill says: glance at me and move on.
Each variant has its own fill and a matching shadow colour. The shadow isn’t decoration – it reinforces the semantic meaning by making the capsule glow in its own hue. Success is emerald, error is red, brand is the app’s purple gradient. The shadow is 25% opacity of the fill, creating a soft halo that reads as confidence, not loudness.
Say what happened, not what the user did
The success toasts never say ‘You saved the item.’ They say ‘Saved.’ The subject is the outcome, not the user. It’s shorter, it respects the user’s intelligence, and it doesn’t patronise. When there’s a count – ‘12 items added to Bought Items’ – it’s specific and useful. When there’s a consequence – ‘Your recommendations just got smarter’ – it’s a reward, not a receipt.
The exceptions are deliberate. ‘Your look is ready’ and ‘Your animation is ready’ use ‘your’ because the user waited for something. The possessive is earned. These are delivery notifications, not confirmations.
Two tiers of failure
Error toasts split into two voice registers. ‘Could not save this item’ – soft, the system tried and it didn’t work out. ‘Failed to upload image’ – hard, something broke. The distinction matters because it sets user expectations: ‘could not’ implies trying again might help; ‘failed to’ implies waiting or checking something.
We never blame the user in an error toast. Even validation errors are framed as requirements – ‘Comment cannot be empty’, ‘Display name is required’ – not as mistakes. The descriptions layer adds a next step when one exists: ‘Please try again’, ‘Try again later’, or a specific instruction. Never decorative. Never redundant with the title.
The loading-to-success transition
The most satisfying pattern in the system: a loading toast that makes a promise, then a success toast that delivers on it – in place, same capsule, with a pulse. ‘Saving from clipboard...’ becomes ‘Saved’. ‘Downloading images (3/12)...’ becomes ‘Exported 12 posts’. The user sees one object transform, not two objects appear and disappear. Continuity.
The implementation uses a string id. toast.loading('Saving...’, { id: ‘clipboard-save’ }) registers the toast. toast.success('Saved’, { id: ‘clipboard-save’ }) finds the existing entry by id and replaces its variant, title, and icon in place. The bump counter triggers the pulse so the state change is visible even if the user looked away.
Loading toasts are also undismissable – drag is disabled, tap does nothing. They persist until explicitly replaced. This prevents the user from accidentally dismissing a progress indicator and wondering if the operation is still running.
Drag to dismiss
The toast resists being dragged downward (elastic: 0.08) but flies away freely upward (elastic: 1.0). The directionality is intentional: toasts arrive from above, so dismissing them means sending them back. Dragging down feels physically wrong – the resistance tells you so. A velocity threshold of -300 means a quick flick works even without hitting the distance threshold of 20px.
Micro-decisions that reveal taste
‘Check-in deleted – you can redo it now.’ The en-dash and consequence. Not just confirming deletion but removing anxiety about permanence.
‘Style profile activated!’ with the description ‘Your picks just got a lot more you.’ The exclamation mark is earned – this is a real product unlock. ‘More you’ is deliberately informal because the feature is personal.
‘You’ve got 5 going already. Give it a sec.’ Rate limiting as a friendly nudge, not a wall. The period after ‘sec’ is casual confidence.
‘Thanks so much for the feedback – you’re helping WNTD become more personalised to you!’ The longest toast in the app. And it should be. Feedback is a gift from the user; the response should feel proportional.
The entry animation
Every toast enters with opacity 0, y: -20, scale: 0.85, and a 6px blur. It springs into place at stiffness 500, damping 28. The blur clearing is what makes it feel like materialisation rather than just a slide-in – the toast doesn’t arrive from somewhere, it appears from nothing. The exit mirrors it: scale 0.9, 4px blur, opacity 0. Smaller blur on exit because the user has already read it – the departure should be quieter than the arrival.
The best toast is the one the user never consciously reads but always trusts. Every string, every animation curve, every directional constraint is in service of that invisible confidence. 230 lines. But the hard part was deciding what those 230 lines should say.
Related
The shrinking gap – why I left Webflow and rebuilt with React and AI
Visual builders don’t shrink the distance between your idea and the finished product. They replace it with a different gap. I rebuilt an actress portfolio from Webflow to React + Claude in a weekend – here’s what changed and what it means for the no-code vs code debate.
Your AI chat isn't bad at recommendations. It's bad at confidence.
Most AI shopping chats ignore behavioral psychology completely. They return good products in a format that makes them look arbitrary.