Bubble Burst Button
Click the button and it gives a little squeeze as a froth of glossy bubbles bursts out all round its edge. They drift outwards, wobbling, and pop one by one with a tiny ring of spray.
How to use it
- Paste the snippet just before
</head>. - Add
data-bubble-burst-buttonto a button.
Options
Add any of these attributes to the same element.
data-colors | Comma-separated bubble colours. Default #a855f7,#ec4899,#38bdf8,#f59e0b. |
data-count | Bubbles per click. Default 14. |
data-spread | How far they drift, in pixels. Default 44. |
data-autoplay | It clicks itself every couple of seconds while in view. Handy for demos. |
From code, animacious.bubbleBurst(button) bursts once. Nothing bursts with reduced motion.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Bubble Burst Button: click the button and it gives a little squeeze as a
// froth of glossy bubbles bursts out all round its edge. They drift
// outwards, wobbling, and pop one by one with a tiny ring.
// Usage: <button data-bubble-burst-button>Celebrate</button>
// Optional: data-colors (comma-separated bubble colours, default
// #a855f7,#ec4899,#38bdf8,#f59e0b), data-count (bubbles per click, default
// 14), data-spread (how far they drift in px, default 44), data-autoplay (it
// clicks itself every couple of seconds while in view; handy for demos).
// Script use: animacious.bubbleBurst(el) bursts once.
// Nothing bursts with reduced motion.
var DEFAULTS = { colors: '#a855f7,#ec4899,#38bdf8,#f59e0b', count: 14, spread: 44, every: 2400 };
var css =
'.bbb{position:relative}' +
'.bbb-layer{position:absolute;inset:0;pointer-events:none;overflow:visible}' +
'.bbb-b{position:absolute;left:0;top:0;border-radius:50%;border:1.5px solid var(--bbb-c);' +
'background:radial-gradient(circle at 32% 30%,rgba(255,255,255,.95) 0 12%,transparent 18%),' +
'radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--bbb-c) 10%,transparent) 55%,color-mix(in srgb,var(--bbb-c) 40%,transparent))}' +
'.bbb-pop{position:absolute;left:0;top:0;border-radius:50%;border:2px dotted var(--bbb-c)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function burst(el) {
if (reduced() || !el._bbbLayer) return;
var d = el.dataset;
var colors = (d.colors || DEFAULTS.colors).split(',');
var count = Math.min(40, +d.count || DEFAULTS.count);
var spread = +d.spread || DEFAULTS.spread;
var w = el.offsetWidth, h = el.offsetHeight;
var R = anime.utils.random;
var layer = el._bbbLayer;
anime.animate(el, {
scale: [{ to: 0.94, duration: 90, ease: 'out(2)' }, { to: 1, duration: 600, ease: 'outElastic(1, .45)' }]
});
for (var i = 0; i < count; i++) {
(function (i) {
// A spot round the edge of an ellipse hugging the button, and the
// direction straight out from it.
var ang = (i / count) * Math.PI * 2 + R(-20, 20) / 100;
var nx = Math.cos(ang), ny = Math.sin(ang);
var sx = w / 2 + nx * w * 0.42, sy = h / 2 + ny * h * 0.42;
var size = R(8, 20);
var dist = spread * R(45, 110) / 100;
var life = R(650, 1150);
var b = document.createElement('span');
b.className = 'bbb-b';
b.style.setProperty('--bbb-c', colors[i % colors.length].trim());
b.style.width = b.style.height = size + 'px';
layer.appendChild(b);
var x0 = sx - size / 2, y0 = sy - size / 2;
var x1 = x0 + nx * dist, y1 = y0 + ny * dist * 0.8 - R(4, 16);
anime.animate(b, {
x: [x0, x1], y: [y0, y1],
scale: [{ from: 0, to: 1, duration: 260, ease: 'outBack(2)' }],
duration: life,
delay: R(0, 90),
ease: 'out(3)',
onComplete: function () {
// Pop: a quick swell, then a ring of spray where it was.
anime.animate(b, {
scale: 1.35, opacity: 0, duration: 120, ease: 'out(2)',
onComplete: function () { b.remove(); }
});
var p = document.createElement('span');
p.className = 'bbb-pop';
p.style.setProperty('--bbb-c', colors[i % colors.length].trim());
p.style.width = p.style.height = size + 'px';
layer.appendChild(p);
anime.animate(p, {
x: x1, y: y1, scale: [0.6, 1.9], opacity: [0.9, 0],
duration: 320, ease: 'out(2)',
onComplete: function () { p.remove(); }
});
}
});
// A little sideways wobble as it drifts.
anime.animate(b, { scaleX: [1, 1.08, 0.95, 1], scaleY: [1, 0.93, 1.05, 1], duration: life, ease: 'inOut(2)' });
})(i);
}
}
function setup(el) {
var d = el.dataset;
el.classList.add('bbb');
var layer = document.createElement('span');
layer.className = 'bbb-layer';
layer.setAttribute('aria-hidden', 'true');
el.appendChild(layer);
el._bbbLayer = layer;
el.addEventListener('click', function () { burst(el); });
if (!reduced() && d.autoplay !== undefined && d.autoplay !== 'false') {
var timer = null;
var tick = function () {
if (!el.matches(':hover')) burst(el);
timer = setTimeout(tick, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(tick, 500);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-bubble-burst-button]').forEach(function (el) {
if (el._bbb) return;
el._bbb = true;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.bubbleBurst = burst;
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button data-bubble-burst-button>Celebrate</button>
<!-- more bubbles, in blues: -->
<button data-bubble-burst-button data-count="20" data-colors="#38bdf8,#818cf8,#22d3ee">Splash</button>