You Might Also Like
Liquid Button — Free HTML CSS JS Gooey Hover Snippet
Liquid Button · Buttons · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Liquid Button — Gooey Metaball Fill With an SVG Goo Filter

The liquid button is the gooey hover effect where the button's fill rises from the bottom as a cluster of blobs that merge into one organic liquid surface — the classic "metaball" look. This snippet builds it with plain HTML, an SVG filter for the goo, CSS, and vanilla JavaScript using the Web Animations API to launch the blobs.
The SVG goo filter
The magic is a reusable SVG filter, #lqGoo. It blurs the source graphic with feGaussianBlur, then runs the blurred result through a feColorMatrix that cranks the alpha channel's contrast (0 0 0 20 -9 on the alpha row). The high alpha multiply plus the negative bias turns the soft blurred edges into a hard threshold: anywhere the blur is dense enough becomes fully opaque, anywhere it's faint becomes transparent. The effect is that separate blurred circles which overlap fuse into a single smooth blob with organic necks between them — exactly how liquid metaballs merge. Applying filter: url(#lqGoo) to the blob container makes every circle inside it gooey.
Blobs that rise and merge
On pointerenter, the script fires nine blobs in quick succession (35ms apart), each positioned at an even horizontal slot across the button. Each blob animates with the Web Animations API from below the button (translateY(40px) scale(0)) up into place at scale(1.5), using a springy cubic-bezier(.34, 1.4, .5, 1) that overshoots. Because the blobs are large and adjacent, they overlap as they rise — and the goo filter fuses them into one wobbling liquid fill that climbs the button. A solid base fill (the ::before) sits under them so the final state is a clean filled pill.
Draining on exit
On pointerleave, each existing blob animates back down and shrinks to zero on a slight stagger, then removes itself in the onfinish callback. The staggered drain makes the liquid appear to recede and drip away rather than vanishing instantly. Pending entrance timers are cleared first so a quick hover-in-hover-out doesn't leave orphaned blobs mid-flight.
Label above the goo
The text label sits at z-index: 2 with pointer-events: none, above the gooey fill layer, so it stays crisp and readable while the liquid animates behind it. isolation: isolate on the button contains the filter's compositing to the button itself. Clicking swaps the label to a "Subscribed ✓" confirmation for a moment.
Why the Web Animations API
Each blob needs its own timed, self-cleaning animation, and element.animate() returns a handle with an onfinish hook — perfect for fire-and-forget blobs that remove themselves when done. It avoids juggling CSS animation classes and animationend listeners across nine short-lived elements.
Customizing it
Change the blob count and size for a chunkier or finer liquid, tune the stdDeviation and the alpha matrix for more or less gooeyness, recolor the blobs and base fill, or adjust the spring easing. Make it a link by swapping the element. Pair it with a shimmer button or place it in an animated gradient CTA for a lively call to action.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not have to decode the feColorMatrix values from memory. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the alpha row "0 0 0 20 -9" in the feColorMatrix turns a soft feGaussianBlur into the hard-edged metaball threshold that fuses overlapping circles, and why isolation: isolate on the button matters for where that filter's compositing is contained. The same assistant can help optimize it, for instance asking whether firing nine separate element.animate() calls with staggered setTimeout delays on every pointerenter could be simplified without losing the springy stagger, or how to guard against orphaned blobs if a user hovers in and out rapidly. It is also useful for extending the effect: ask it to make the blob color shift on click, add a second goo layer with a different blur radius for a thicker liquid look, or adapt the same filter technique to a gooey navigation menu. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "liquid button" with a gooey metaball hover fill in plain HTML, CSS, and JavaScript, using an SVG filter for the goo effect and the Web Animations API for the blobs — no canvas, no libraries.
Requirements:
- A single reusable SVG filter (referenced once via filter: url(#id)) built from a feGaussianBlur with a noticeable stdDeviation, feeding into a feColorMatrix whose alpha row sharply increases contrast (a large positive multiplier and a negative bias) so that overlapping blurred shapes fuse into one hard-edged blob instead of staying soft and translucent.
- A button containing a text label positioned above the gooey layer with a higher z-index and pointer-events: none, so the goo animation never intercepts clicks or blurs the text, and isolation: isolate on the button so the filter's compositing does not leak onto surrounding elements.
- On pointerenter, launch a fixed number of circular blob elements (e.g. nine) at staggered short delays across evenly spaced horizontal positions along the button, each animated with element.animate() from below the button at scale 0 up to an overshooting scale using a springy cubic-bezier easing, so they rise and overlap into the gooey fill layer.
- On pointerleave, animate each currently-present blob back down and to scale 0 on a staggered delay (not all at once), removing each blob element from the DOM in its animation's onfinish callback so nothing accumulates.
- Any pending entrance timers must be cancelled if the pointer leaves before all blobs have launched, so a fast hover-in-hover-out never leaves orphaned blobs mid-animation.
- Clicking the button must not interrupt or reset the goo animation; it should only swap the label text to a temporary confirmation state for a couple of seconds before reverting.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA rounded button renders with a label.
- 2Hover the buttonGooey blobs rise from the bottom and merge into a liquid fill.
- 3Leave the buttonThe liquid drains and drips away on a stagger.
- 4Click itThe label flips to a Subscribed confirmation briefly.
- 5Tune the gooAdjust the blur and alpha matrix for more or less merge.
- 6Recolor itChange the blob and base fill colors.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The blob container has an SVG goo filter applied: feGaussianBlur softens the circles, then feColorMatrix sharply increases the alpha channel's contrast (multiply 20, bias -9). That thresholds the blur so dense overlaps become fully opaque and faint edges vanish, fusing overlapping circles into a single smooth blob with organic necks — the metaball effect.
On hover, nine blobs are launched 35ms apart at even horizontal slots. Each animates with the Web Animations API from below the button at scale 0 up to scale 1.5 using a springy cubic-bezier(.34,1.4,.5,1) that overshoots. Being large and adjacent, they overlap as they climb, and the goo filter fuses them into one liquid fill.
Each blob is a short-lived element that needs its own timed animation and must remove itself when done. element.animate() returns a handle with an onfinish callback, perfect for fire-and-forget blobs, and avoids managing CSS animation classes and animationend listeners across nine elements that come and go.
Yes. The label sits at z-index 2 with pointer-events: none, above the gooey fill layer, so it remains crisp while the liquid animates behind it. isolation: isolate on the button keeps the filter's compositing contained so it doesn't affect surrounding content.
Include the SVG filter once in your app (it can live in a shared layout). Render the button as a component and trigger the blob animations from pointerenter/pointerleave handlers that append blobs to a ref'd container with element.animate, cleaning them up on finish and clearing timers on unmount. In Tailwind, style the pill with utilities and keep the goo filter and blob logic in the component.