More Buttons Snippets
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.
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.
Liquid Button — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Liquid Button</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;display:flex;justify-content:center;align-items:center;min-height:100vh}
.lq-stage{padding:40px}
.lq-btn{position:relative;border:none;background:none;padding:16px 36px;border-radius:999px;cursor:pointer;font-family:inherit;font-size:16px;font-weight:800;color:#fff;isolation:isolate}
.lq-label{position:relative;z-index:2;pointer-events:none}
.lq-blobs{position:absolute;inset:0;z-index:1;border-radius:inherit;filter:url(#lqGoo);overflow:visible}
.lq-blobs::before{content:'';position:absolute;inset:0;border-radius:inherit;background:#6366f1}
.lq-blob{position:absolute;top:50%;width:34px;height:34px;border-radius:50%;background:#6366f1;transform:translate(-50%,-50%) scale(0)}
</style>
</head>
<body>
<div class="lq-stage">
<svg width="0" height="0" class="lq-defs"><filter id="lqGoo"><feGaussianBlur in="SourceGraphic" stdDeviation="8" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo"/><feBlend in="SourceGraphic" in2="goo"/></filter></svg>
<button type="button" class="lq-btn" id="lqBtn">
<span class="lq-blobs" id="lqBlobs"></span>
<span class="lq-label">Subscribe</span>
</button>
</div>
<script>
var btn = document.getElementById('lqBtn');
var host = document.getElementById('lqBlobs');
// On hover, fire a series of gooey blobs that rise from the bottom edge. The SVG
// goo filter merges any overlapping circles into one organic liquid mass.
var timers = [];
function fluidIn() {
clear();
var r = btn.getBoundingClientRect();
var n = 9;
for (var i = 0; i < n; i++) {
(function (i) {
timers.push(setTimeout(function () {
var b = document.createElement('span');
b.className = 'lq-blob';
var x = (i + 0.5) / n * 100;
b.style.left = x + '%';
host.appendChild(b);
b.animate(
[{ transform: 'translate(-50%,40px) scale(0)' },
{ transform: 'translate(-50%,-50%) scale(1.5)', offset: .6 },
{ transform: 'translate(-50%,-50%) scale(1.5)' }],
{ duration: 600, easing: 'cubic-bezier(.34,1.4,.5,1)', fill: 'forwards' }
);
}, i * 35));
})(i);
}
}
function fluidOut() {
clear();
Array.prototype.slice.call(host.querySelectorAll('.lq-blob')).forEach(function (b, i) {
setTimeout(function () {
b.animate([{ transform: getComputedStyle(b).transform },
{ transform: 'translate(-50%,40px) scale(0)' }],
{ duration: 400, easing: 'ease-in', fill: 'forwards' }).onfinish = function () { b.remove(); };
}, i * 25);
});
}
function clear() { timers.forEach(clearTimeout); timers = []; }
btn.addEventListener('pointerenter', fluidIn);
btn.addEventListener('pointerleave', fluidOut);
btn.addEventListener('click', function () {
var l = btn.querySelector('.lq-label');
l.textContent = 'Subscribed ✓';
setTimeout(function () { l.textContent = 'Subscribe'; }, 1400);
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Liquid Button</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;display:flex;justify-content:center;align-items:center;min-height:100vh
}
.lq-blobs::before {
content:'';position:absolute;inset:0;border-radius:inherit;background:#6366f1
}
.lq-blob {
position:absolute;top:50%;width:34px;height:34px;border-radius:50%;background:#6366f1;transform:translate(-50%,-50%) scale(0)
}
</style>
</head>
<body>
<div class="p-10">
<svg width="0" height="0" class="lq-defs"><filter id="lqGoo"><feGaussianBlur in="SourceGraphic" stdDeviation="8" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo"/><feBlend in="SourceGraphic" in2="goo"/></filter></svg>
<button type="button" class="relative border-0 bg-transparent py-4 px-9 rounded-[999px] cursor-pointer font-[inherit] text-base font-extrabold text-[#fff] [isolation:isolate]" id="lqBtn">
<span class="lq-blobs absolute inset-0 z-[1] rounded-[inherit] [filter:url(#lqGoo)] overflow-visible" id="lqBlobs"></span>
<span class="lq-label relative z-[2] pointer-events-none">Subscribe</span>
</button>
</div>
<script>
var btn = document.getElementById('lqBtn');
var host = document.getElementById('lqBlobs');
// On hover, fire a series of gooey blobs that rise from the bottom edge. The SVG
// goo filter merges any overlapping circles into one organic liquid mass.
var timers = [];
function fluidIn() {
clear();
var r = btn.getBoundingClientRect();
var n = 9;
for (var i = 0; i < n; i++) {
(function (i) {
timers.push(setTimeout(function () {
var b = document.createElement('span');
b.className = 'lq-blob';
var x = (i + 0.5) / n * 100;
b.style.left = x + '%';
host.appendChild(b);
b.animate(
[{ transform: 'translate(-50%,40px) scale(0)' },
{ transform: 'translate(-50%,-50%) scale(1.5)', offset: .6 },
{ transform: 'translate(-50%,-50%) scale(1.5)' }],
{ duration: 600, easing: 'cubic-bezier(.34,1.4,.5,1)', fill: 'forwards' }
);
}, i * 35));
})(i);
}
}
function fluidOut() {
clear();
Array.prototype.slice.call(host.querySelectorAll('.lq-blob')).forEach(function (b, i) {
setTimeout(function () {
b.animate([{ transform: getComputedStyle(b).transform },
{ transform: 'translate(-50%,40px) scale(0)' }],
{ duration: 400, easing: 'ease-in', fill: 'forwards' }).onfinish = function () { b.remove(); };
}, i * 25);
});
}
function clear() { timers.forEach(clearTimeout); timers = []; }
btn.addEventListener('pointerenter', fluidIn);
btn.addEventListener('pointerleave', fluidOut);
btn.addEventListener('click', function () {
var l = btn.querySelector('.lq-label');
l.textContent = 'Subscribed ✓';
setTimeout(function () { l.textContent = 'Subscribe'; }, 1400);
});
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to LiquidButton.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;display:flex;justify-content:center;align-items:center;min-height:100vh}
.lq-stage{padding:40px}
.lq-btn{position:relative;border:none;background:none;padding:16px 36px;border-radius:999px;cursor:pointer;font-family:inherit;font-size:16px;font-weight:800;color:#fff;isolation:isolate}
.lq-label{position:relative;z-index:2;pointer-events:none}
.lq-blobs{position:absolute;inset:0;z-index:1;border-radius:inherit;filter:url(#lqGoo);overflow:visible}
.lq-blobs::before{content:'';position:absolute;inset:0;border-radius:inherit;background:#6366f1}
.lq-blob{position:absolute;top:50%;width:34px;height:34px;border-radius:50%;background:#6366f1;transform:translate(-50%,-50%) scale(0)}
`;
export default function LiquidButton() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var btn = document.getElementById('lqBtn');
var host = document.getElementById('lqBlobs');
// On hover, fire a series of gooey blobs that rise from the bottom edge. The SVG
// goo filter merges any overlapping circles into one organic liquid mass.
var timers = [];
function fluidIn() {
clear();
var r = btn.getBoundingClientRect();
var n = 9;
for (var i = 0; i < n; i++) {
(function (i) {
timers.push(setTimeout(function () {
var b = document.createElement('span');
b.className = 'lq-blob';
var x = (i + 0.5) / n * 100;
b.style.left = x + '%';
host.appendChild(b);
b.animate(
[{ transform: 'translate(-50%,40px) scale(0)' },
{ transform: 'translate(-50%,-50%) scale(1.5)', offset: .6 },
{ transform: 'translate(-50%,-50%) scale(1.5)' }],
{ duration: 600, easing: 'cubic-bezier(.34,1.4,.5,1)', fill: 'forwards' }
);
}, i * 35));
})(i);
}
}
function fluidOut() {
clear();
Array.prototype.slice.call(host.querySelectorAll('.lq-blob')).forEach(function (b, i) {
setTimeout(function () {
b.animate([{ transform: getComputedStyle(b).transform },
{ transform: 'translate(-50%,40px) scale(0)' }],
{ duration: 400, easing: 'ease-in', fill: 'forwards' }).onfinish = function () { b.remove(); };
}, i * 25);
});
}
function clear() { timers.forEach(clearTimeout); timers = []; }
btn.addEventListener('pointerenter', fluidIn);
btn.addEventListener('pointerleave', fluidOut);
btn.addEventListener('click', function () {
var l = btn.querySelector('.lq-label');
l.textContent = 'Subscribed ✓';
setTimeout(function () { l.textContent = 'Subscribe'; }, 1400);
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="lq-stage">
<svg width="0" height="0" className="lq-defs"><filter id="lqGoo"><feGaussianBlur in="SourceGraphic" stdDeviation="8" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo"/><feBlend in="SourceGraphic" in2="goo"/></filter></svg>
<button type="button" className="lq-btn" id="lqBtn">
<span className="lq-blobs" id="lqBlobs"></span>
<span className="lq-label">Subscribe</span>
</button>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function LiquidButton() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var btn = document.getElementById('lqBtn');
var host = document.getElementById('lqBlobs');
// On hover, fire a series of gooey blobs that rise from the bottom edge. The SVG
// goo filter merges any overlapping circles into one organic liquid mass.
var timers = [];
function fluidIn() {
clear();
var r = btn.getBoundingClientRect();
var n = 9;
for (var i = 0; i < n; i++) {
(function (i) {
timers.push(setTimeout(function () {
var b = document.createElement('span');
b.className = 'lq-blob';
var x = (i + 0.5) / n * 100;
b.style.left = x + '%';
host.appendChild(b);
b.animate(
[{ transform: 'translate(-50%,40px) scale(0)' },
{ transform: 'translate(-50%,-50%) scale(1.5)', offset: .6 },
{ transform: 'translate(-50%,-50%) scale(1.5)' }],
{ duration: 600, easing: 'cubic-bezier(.34,1.4,.5,1)', fill: 'forwards' }
);
}, i * 35));
})(i);
}
}
function fluidOut() {
clear();
Array.prototype.slice.call(host.querySelectorAll('.lq-blob')).forEach(function (b, i) {
setTimeout(function () {
b.animate([{ transform: getComputedStyle(b).transform },
{ transform: 'translate(-50%,40px) scale(0)' }],
{ duration: 400, easing: 'ease-in', fill: 'forwards' }).onfinish = function () { b.remove(); };
}, i * 25);
});
}
function clear() { timers.forEach(clearTimeout); timers = []; }
btn.addEventListener('pointerenter', fluidIn);
btn.addEventListener('pointerleave', fluidOut);
btn.addEventListener('click', function () {
var l = btn.querySelector('.lq-label');
l.textContent = 'Subscribed ✓';
setTimeout(function () { l.textContent = 'Subscribe'; }, 1400);
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;display:flex;justify-content:center;align-items:center;min-height:100vh
}
.lq-blobs::before {
content:'';position:absolute;inset:0;border-radius:inherit;background:#6366f1
}
.lq-blob {
position:absolute;top:50%;width:34px;height:34px;border-radius:50%;background:#6366f1;transform:translate(-50%,-50%) scale(0)
}
`}</style>
<div className="p-10">
<svg width="0" height="0" className="lq-defs"><filter id="lqGoo"><feGaussianBlur in="SourceGraphic" stdDeviation="8" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo"/><feBlend in="SourceGraphic" in2="goo"/></filter></svg>
<button type="button" className="relative border-0 bg-transparent py-4 px-9 rounded-[999px] cursor-pointer font-[inherit] text-base font-extrabold text-[#fff] [isolation:isolate]" id="lqBtn">
<span className="lq-blobs absolute inset-0 z-[1] rounded-[inherit] [filter:url(#lqGoo)] overflow-visible" id="lqBlobs"></span>
<span className="lq-label relative z-[2] pointer-events-none">Subscribe</span>
</button>
</div>
</>
);
}<template>
<div class="lq-stage">
<svg width="0" height="0" class="lq-defs"><filter id="lqGoo"><feGaussianBlur in="SourceGraphic" stdDeviation="8" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo"/><feBlend in="SourceGraphic" in2="goo"/></filter></svg>
<button type="button" class="lq-btn" id="lqBtn">
<span class="lq-blobs" id="lqBlobs"></span>
<span class="lq-label">Subscribe</span>
</button>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let btn;
let host;
// On hover, fire a series of gooey blobs that rise from the bottom edge. The SVG
// goo filter merges any overlapping circles into one organic liquid mass.
var timers = [];
function fluidIn() {
clear();
var r = btn.getBoundingClientRect();
var n = 9;
for (var i = 0; i < n; i++) {
(function (i) {
timers.push(setTimeout(function () {
var b = document.createElement('span');
b.className = 'lq-blob';
var x = (i + 0.5) / n * 100;
b.style.left = x + '%';
host.appendChild(b);
b.animate(
[{ transform: 'translate(-50%,40px) scale(0)' },
{ transform: 'translate(-50%,-50%) scale(1.5)', offset: .6 },
{ transform: 'translate(-50%,-50%) scale(1.5)' }],
{ duration: 600, easing: 'cubic-bezier(.34,1.4,.5,1)', fill: 'forwards' }
);
}, i * 35));
})(i);
}
}
function fluidOut() {
clear();
Array.prototype.slice.call(host.querySelectorAll('.lq-blob')).forEach(function (b, i) {
setTimeout(function () {
b.animate([{ transform: getComputedStyle(b).transform },
{ transform: 'translate(-50%,40px) scale(0)' }],
{ duration: 400, easing: 'ease-in', fill: 'forwards' }).onfinish = function () { b.remove(); };
}, i * 25);
});
}
function clear() { timers.forEach(clearTimeout); timers = []; }
onMounted(() => {
btn = document.getElementById('lqBtn');
host = document.getElementById('lqBlobs');
btn.addEventListener('pointerenter', fluidIn);
btn.addEventListener('pointerleave', fluidOut);
btn.addEventListener('click', function () {
var l = btn.querySelector('.lq-label');
l.textContent = 'Subscribed ✓';
setTimeout(function () { l.textContent = 'Subscribe'; }, 1400);
});
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;display:flex;justify-content:center;align-items:center;min-height:100vh}
.lq-stage{padding:40px}
.lq-btn{position:relative;border:none;background:none;padding:16px 36px;border-radius:999px;cursor:pointer;font-family:inherit;font-size:16px;font-weight:800;color:#fff;isolation:isolate}
.lq-label{position:relative;z-index:2;pointer-events:none}
.lq-blobs{position:absolute;inset:0;z-index:1;border-radius:inherit;filter:url(#lqGoo);overflow:visible}
.lq-blobs::before{content:'';position:absolute;inset:0;border-radius:inherit;background:#6366f1}
.lq-blob{position:absolute;top:50%;width:34px;height:34px;border-radius:50%;background:#6366f1;transform:translate(-50%,-50%) scale(0)}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-liquid-button',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="lq-stage">
<svg width="0" height="0" class="lq-defs"><filter id="lqGoo"><feGaussianBlur in="SourceGraphic" stdDeviation="8" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo"/><feBlend in="SourceGraphic" in2="goo"/></filter></svg>
<button type="button" class="lq-btn" id="lqBtn">
<span class="lq-blobs" id="lqBlobs"></span>
<span class="lq-label">Subscribe</span>
</button>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;display:flex;justify-content:center;align-items:center;min-height:100vh}
.lq-stage{padding:40px}
.lq-btn{position:relative;border:none;background:none;padding:16px 36px;border-radius:999px;cursor:pointer;font-family:inherit;font-size:16px;font-weight:800;color:#fff;isolation:isolate}
.lq-label{position:relative;z-index:2;pointer-events:none}
.lq-blobs{position:absolute;inset:0;z-index:1;border-radius:inherit;filter:url(#lqGoo);overflow:visible}
.lq-blobs::before{content:'';position:absolute;inset:0;border-radius:inherit;background:#6366f1}
.lq-blob{position:absolute;top:50%;width:34px;height:34px;border-radius:50%;background:#6366f1;transform:translate(-50%,-50%) scale(0)}
`]
})
export class LiquidButtonComponent implements AfterViewInit {
ngAfterViewInit(): void {
var btn = document.getElementById('lqBtn');
var host = document.getElementById('lqBlobs');
// On hover, fire a series of gooey blobs that rise from the bottom edge. The SVG
// goo filter merges any overlapping circles into one organic liquid mass.
var timers = [];
function fluidIn() {
clear();
var r = btn.getBoundingClientRect();
var n = 9;
for (var i = 0; i < n; i++) {
(function (i) {
timers.push(setTimeout(function () {
var b = document.createElement('span');
b.className = 'lq-blob';
var x = (i + 0.5) / n * 100;
b.style.left = x + '%';
host.appendChild(b);
b.animate(
[{ transform: 'translate(-50%,40px) scale(0)' },
{ transform: 'translate(-50%,-50%) scale(1.5)', offset: .6 },
{ transform: 'translate(-50%,-50%) scale(1.5)' }],
{ duration: 600, easing: 'cubic-bezier(.34,1.4,.5,1)', fill: 'forwards' }
);
}, i * 35));
})(i);
}
}
function fluidOut() {
clear();
Array.prototype.slice.call(host.querySelectorAll('.lq-blob')).forEach(function (b, i) {
setTimeout(function () {
b.animate([{ transform: getComputedStyle(b).transform },
{ transform: 'translate(-50%,40px) scale(0)' }],
{ duration: 400, easing: 'ease-in', fill: 'forwards' }).onfinish = function () { b.remove(); };
}, i * 25);
});
}
function clear() { timers.forEach(clearTimeout); timers = []; }
btn.addEventListener('pointerenter', fluidIn);
btn.addEventListener('pointerleave', fluidOut);
btn.addEventListener('click', function () {
var l = btn.querySelector('.lq-label');
l.textContent = 'Subscribed ✓';
setTimeout(function () { l.textContent = 'Subscribe'; }, 1400);
});
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { fluidIn, fluidOut, clear });
}
}