Inspiration

Most toast libraries are just colored rectangles that slide in and fade out. Nobody notices them. Nobody's supposed to. I found goey-toast for React and the animation stopped me. A pill that breathes open, morphs into a blob, then closes back. It felt alive. Vue didn't have it, so I built it

What it does

vue-goey-toast brings gooey, morphing toast notifications to Vue 3. Toasts expand from a pill into an organic blob shape, then collapse back on dismiss. It supports five toast types, promise toasts, action buttons, six positions, dark mode, RTL, swipe to dismiss, keyboard shortcuts, and a single bounce prop that controls how dramatic the spring physics feel.

How we built it

I separated concerns cleanly. vue-sonner handles the queue, positioning, and lifecycle. GooeyToast.vue owns the SVG morph and spring animation. The gooeyToast API wraps both. Neither layer knows too much about the other. The morph itself is SVG path interpolation: two shapes described as Bézier curves, with the engine walking between them frame by frame:

P(t) = (1 - t) \cdot P_{\text{pill}}(t) + t \cdot P_{\text{blob}}(t), \quad t \in [0, 1]

The toast lifecycle runs a simple state machine: IDLE -> EXPANDING -> EXPANDED -> COLLAPSING -> COLLAPSED -> (unmount) ^ | (hover) (hover re-expand) Each transition has its own spring config. The whole thing was built on an Android phone via Termux.

Challenges we ran into

Getting the blob to open in the right direction for all six positions was tedious. The fix was one canonical blob shape plus a transform pipeline. Horizontal reflection across center x = c:

x' = 2c - x

Simple math, but one control point off by a pixel and the morph looks broken at that position. Porting from React to Vue was the other challenge. Vue's reactivity model isn't just different syntax. Translating useEffect patterns directly produces code that technically works but feels wrong. I had to unlearn React to write idiomatic Vue.

Accomplishments that we're proud of

The bounce prop. Under the hood it drives both stiffness k and damping c:

k = k_{\min} + b \cdot (k_{\max} - k_{\min}), \quad c = c_{\max} - b \cdot (c_{\max} - c_{\min})

One number, consistent feel across every animation in the component. That kind of API simplicity takes more work than exposing both values directly, and I think it shows. Also, the whole thing was written on a phone. That still feels worth mentioning

What we learned

Animation is harder than it looks. The morph isn't CSS trickery, it's geometry. Getting the control points right so shapes transition smoothly without twisting took real iteration. Library authorship is its own discipline too. Peer dependency ranges, tree-shakeable exports, type export surfaces, .npmignore hygiene. None of it is hard, but every piece you get wrong is a papercut for the developer who installs your package.

What's next for vue-goey-toast

A visual playground for building and previewing toast configs with live code output, and proper prefers-reduced-motion support that collapses all spring and morph animations to simple fades automatically.

Built With

  • motion-v
  • vue
  • vue-sonner
Share this project:

Updates