← Particles

Particle Explosion

Click an element and it shatters into little square pieces that fly out from the spot you clicked, tumble, shrink into dots and fade. Then they rewind and pull back together, or the element is gone for good.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-particle-explosion to a button, card or image. It explodes on click.

Options

Add any of these attributes to the same element.

data-removeDon’t come back: remove the element afterwards, e.g. a dismissed card.
data-delayTime before the pieces fly back. Default 900 ms.
data-piecesRoughly how many pieces. Default 140.
data-spreadHow far they fly, as a share of the element’s size. Default 1.
data-loopExplode by itself every few seconds while in view. Handy for demos.

The pieces are real copies of the element, so text, images and gradients all break up. Keyboard clicks burst from the middle. With reduced motion it just fades. From code, animacious.explode(element) sets it off.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Particle Explosion: click an element and it shatters into little square
    // pieces that fly apart from the spot you clicked, tumble, shrink into
    // dots and fade, then pull back together. Or make it vanish for good.
    // Usage: <button data-particle-explosion>Click me</button>
    // Optional: data-remove (don't come back: the element is removed),
    // data-delay (ms before the pieces fly back, default 900), data-loop (it
    // explodes by itself every few seconds while in view; good for demos),
    // data-pieces (roughly how many, default 140), data-spread (how far they
    // fly, as a share of the element's size, default 1).
    // Script use: animacious.explode(element) explodes it from code.
    var DEFAULTS = { delay: 900, pieces: 140, spread: 1, every: 3400 };

    var css =
      '.pex-layer{position:absolute;pointer-events:none;z-index:5}' +
      '.pex-piece{position:absolute;overflow:hidden;will-change:transform,opacity}' +
      '.pex-piece>*{position:absolute!important;margin:0!important;box-sizing:border-box;transform:none!important}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function explode(el, ox, oy) {
      if (el._pexBusy) return;
      var d = el.dataset;
      var remove = d.remove !== undefined && d.remove !== 'false';
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        // No flying pieces: just fade out (and back in).
        el._pexBusy = true;
        anime.animate(el, {
          opacity: remove ? 0 : [1, 0.25, 1],
          duration: remove ? 300 : 900,
          onComplete: function () { el._pexBusy = false; if (remove) el.remove(); }
        });
        return;
      }
      var w = el.offsetWidth, h = el.offsetHeight;
      if (!w || !h) return;
      el._pexBusy = true;
      // Square pieces, sized so there are about data-pieces of them.
      var size = Math.max(6, Math.sqrt((w * h) / (+d.pieces || DEFAULTS.pieces)));
      var cols = Math.ceil(w / size), rows = Math.ceil(h / size);
      var spread = (+d.spread || DEFAULTS.spread) * Math.max(80, Math.max(w, h) * 0.75);
      ox = ox === undefined ? w / 2 : ox;
      oy = oy === undefined ? h / 2 : oy;

      // A layer exactly over the element, holding a grid of windows onto copies of it.
      var layer = document.createElement('div');
      layer.className = 'pex-layer';
      layer.setAttribute('aria-hidden', 'true');
      layer.style.left = el.offsetLeft + 'px';
      layer.style.top = el.offsetTop + 'px';
      layer.style.width = w + 'px';
      layer.style.height = h + 'px';
      var copy = el.cloneNode(true);
      copy.removeAttribute('data-particle-explosion');
      copy.removeAttribute('id');
      copy.querySelectorAll('[id]').forEach(function (n) { n.removeAttribute('id'); });
      copy.style.width = w + 'px';
      copy.style.height = h + 'px';
      copy.style.opacity = '1';
      var pieces = [];
      for (var r = 0; r < rows; r++) {
        for (var c = 0; c < cols; c++) {
          var p = document.createElement('div');
          p.className = 'pex-piece';
          var pw = Math.min(size, w - c * size), ph = Math.min(size, h - r * size);
          p.style.cssText = 'left:' + c * size + 'px;top:' + r * size + 'px;width:' + pw + 'px;height:' + ph + 'px';
          var cc = copy.cloneNode(true);
          cc.style.left = -c * size + 'px';
          cc.style.top = -r * size + 'px';
          p.appendChild(cc);
          layer.appendChild(p);
          pieces.push({ el: p, x: c * size + pw / 2, y: r * size + ph / 2 });
        }
      }
      el.parentNode.insertBefore(layer, el.nextSibling);
      el.style.opacity = '0';

      var R = anime.utils.random;
      var longest = 0;
      pieces.forEach(function (p) {
        var dx = p.x - ox, dy = p.y - oy, dist = Math.sqrt(dx * dx + dy * dy) || 1;
        var a = Math.atan2(dy, dx) + R(-0.5, 0.5, 2);
        // Pieces near the blast fly furthest.
        var near = 1 - Math.min(1, dist / Math.max(w, h));
        var reach = spread * R(0.35, 1, 2) * (0.6 + near * 0.8);
        var fx = Math.cos(a) * reach, fy = Math.sin(a) * reach;
        var dur = R(650, 1100);
        longest = Math.max(longest, dur);
        anime.animate(p.el, {
          x: { to: fx, ease: 'out(3)' },
          // Up and away, then a little gravity.
          y: [{ to: fy - reach * 0.25, duration: dur * 0.45, ease: 'out(2)' }, { to: fy + reach * 0.3, duration: dur * 0.55, ease: 'in(1.6)' }],
          rotate: { to: R(-200, 200), ease: 'out(2)' },
          scale: { to: R(0.25, 0.55, 2), ease: 'out(2)' },
          borderRadius: { to: '50%', duration: dur * 0.4 },
          opacity: [{ to: 1, duration: dur * 0.5 }, { to: 0, duration: dur * 0.5, ease: 'in(2)' }],
          duration: dur
        });
      });

      var delay = d.delay !== undefined ? +d.delay : DEFAULTS.delay;
      setTimeout(function () {
        if (remove) {
          layer.remove();
          el.remove();
          return;
        }
        // Rewind: every piece flies home, the outer ones last.
        var back = 0;
        pieces.forEach(function (p) {
          var dx = p.x - w / 2, dy = p.y - h / 2;
          var wait = (Math.sqrt(dx * dx + dy * dy) / Math.max(w, h)) * 260 + R(0, 160);
          var dur = R(700, 900);
          back = Math.max(back, wait + dur);
          anime.animate(p.el, {
            x: 0, y: 0, rotate: 0, scale: 1, borderRadius: '0%',
            opacity: { to: 1, duration: dur * 0.5 },
            duration: dur, delay: wait, ease: 'out(4)'
          });
        });
        setTimeout(function () {
          el.style.opacity = '';
          layer.remove();
          anime.animate(el, { scale: [1.04, 1], duration: 400, ease: 'out(3)' });
          el._pexBusy = false;
        }, back);
      }, longest + delay);
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      document.querySelectorAll('[data-particle-explosion]').forEach(function (el) {
        if (el._pex) return;
        el._pex = true;
        el.addEventListener('click', function (e) {
          var r = el.getBoundingClientRect();
          // Keyboard clicks have no position: burst from the middle.
          var fromKey = e.detail === 0;
          explode(el, fromKey ? undefined : e.clientX - r.left, fromKey ? undefined : e.clientY - r.top);
        });
        if (el.dataset.loop === undefined || reduced) return;
        var timer = null;
        new IntersectionObserver(function (entries) {
          clearInterval(timer);
          if (!entries[0].isIntersecting) return;
          timer = setInterval(function () {
            if (el.closest('[hidden]')) return;
            explode(el, el.offsetWidth * anime.utils.random(0.25, 0.75, 2), el.offsetHeight * anime.utils.random(0.3, 0.7, 2));
          }, DEFAULTS.every);
        }, { threshold: 0.5 }).observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.explode = function (el, x, y) { if (el) explode(el, x, y); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<button data-particle-explosion>Click me</button>

<!-- a card that's removed when it's dismissed: -->
<div class="card" data-particle-explosion data-remove>…</div>