← Buttons

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

  1. Paste the snippet just before </head>.
  2. Add data-bubble-burst-button to a button.

Options

Add any of these attributes to the same element.

data-colorsComma-separated bubble colours. Default #a855f7,#ec4899,#38bdf8,#f59e0b.
data-countBubbles per click. Default 14.
data-spreadHow far they drift, in pixels. Default 44.
data-autoplayIt clicks itself every couple of seconds while in view. Handy for demos.

From code, animacious.bubbleBurst(button) bursts once. Nothing bursts with reduced motion.

1 · Snippet
<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>
2 · Your HTML
<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>