Skip to content

4 April 2026

We replaced our toast library with 230 lines. The strings were the hard part.

Product Design·Microcopy·Interaction Design·React

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.

9:41
100%
Saved
Duplicates merge with a count badge and pulse — never stack

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.

Saved
success
Could not savePlease try again.
error
Your look is ready
brand

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.

9:41
100%
Your look is ready
Brand — the app's personality channel

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.

9:41
100%
Could not save this itemPlease try again.
Error with description — say what went wrong, offer a next step

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.

9:41
100%
Saving from clipboard…
Loading morphs into success — same capsule, no flicker

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.

9:41
100%
Look saved!
Drag upward to dismiss — try it

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.

9:41
100%
Saved
Success — confirm what happened, not what the user did

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.