← SVG & icons

Logo Shatter Hover

Point at the logo and it breaks into glassy shards that burst away from the pointer, spinning as they go. Move away and they fly back and lock together. On a phone a tap shatters it, and it puts itself back together.

Point at the logo, or tap it

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-logo-shatter-hover to the element holding your logo: an <img>, an inline <svg>, a wordmark or all three.
  3. Style the logo with a class rather than by its attribute, as the shards are copies of it without the attribute.

Options

Add any of these attributes to the same element.

data-colsColumns the shards are cut from. Default 6.
data-rowsRows the shards are cut from. Default 3.
data-forceHow far the shards fly. Default 1.
data-autoplayIt shatters itself every few seconds while in view, pausing when someone interacts. Handy for demos.

Each shard is a copy of the whole logo clipped to a triangle, so keep the logo light (one image or a simple SVG); the copies are hidden from screen readers and their ids removed. animacious.logoShatter(el) shatters it once from code. With reduced motion it stays whole.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Logo Shatter Hover: point at the logo and it breaks into glassy shards
    // that burst away from the pointer, spinning; move away and they fly back
    // and lock together again. On a touch screen a tap shatters it and it
    // reassembles by itself.
    // Usage: <div data-logo-shatter-hover><img src="/logo.svg" alt="Your brand"></div>
    // Works with any logo: an <img>, an inline <svg>, a wordmark or all three.
    // Optional: data-cols and data-rows (the grid the shards are cut from,
    // default 6 by 3), data-force (how far they fly, default 1), data-autoplay
    // (it shatters itself every few seconds while in view, pausing when someone
    // interacts; handy for demos).
    // Script use: animacious.logoShatter(el) shatters and reassembles it once.
    var DEFAULTS = { cols: 6, rows: 3, force: 1, every: 3400, hold: 900 };

    var css =
      '.lsh{position:relative;cursor:default;-webkit-tap-highlight-color:transparent}' +
      '.lsh-whole{display:contents}' +
      '.lsh.lsh-broken>.lsh-whole{visibility:hidden}' +
      '.lsh-shards{position:absolute;inset:0;pointer-events:none;visibility:hidden}' +
      '.lsh.lsh-broken>.lsh-shards{visibility:visible}' +
      '.lsh-shard{position:absolute!important;left:0!important;top:0!important;width:100%!important;height:100%!important;margin:0!important;box-sizing:border-box;will-change:transform}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function rnd(a, b) { return a + Math.random() * (b - a); }

    // Cut the box into a jittered grid, each cell split corner to corner into
    // two triangles. Points are percentages of the logo's box.
    function cut(cols, rows) {
      var pts = [], r, c;
      for (r = 0; r <= rows; r++) {
        pts.push([]);
        for (c = 0; c <= cols; c++) {
          var x = (c / cols) * 100, y = (r / rows) * 100;
          if (c > 0 && c < cols) x += rnd(-0.32, 0.32) * (100 / cols);
          if (r > 0 && r < rows) y += rnd(-0.32, 0.32) * (100 / rows);
          pts[r].push([x, y]);
        }
      }
      var tris = [];
      for (r = 0; r < rows; r++) {
        for (c = 0; c < cols; c++) {
          var a = pts[r][c], b = pts[r][c + 1], d = pts[r + 1][c], e = pts[r + 1][c + 1];
          if ((r + c) % 2) { tris.push([a, b, e], [a, e, d]); }
          else { tris.push([a, b, d], [b, e, d]); }
        }
      }
      return tris;
    }

    function setup(el) {
      var d = el.dataset;
      // The shards are copies of the whole logo, each clipped to its triangle.
      var copy = el.cloneNode(true);
      copy.removeAttribute('data-logo-shatter-hover');
      copy.removeAttribute('id');
      copy.removeAttribute('role');
      copy.removeAttribute('aria-label');
      copy.querySelectorAll('[id]').forEach(function (n) { n.removeAttribute('id'); });
      el.classList.add('lsh');
      var whole = document.createElement('span');
      whole.className = 'lsh-whole';
      while (el.firstChild) whole.appendChild(el.firstChild);
      el.appendChild(whole);
      var layer = document.createElement('span');
      layer.className = 'lsh-shards';
      layer.setAttribute('aria-hidden', 'true');
      var shards = cut(Math.max(1, +d.cols || DEFAULTS.cols), Math.max(1, +d.rows || DEFAULTS.rows)).map(function (t) {
        var s = copy.cloneNode(true);
        s.classList.add('lsh-shard');
        s.style.clipPath = 'polygon(' + t.map(function (p) { return p[0].toFixed(2) + '% ' + p[1].toFixed(2) + '%'; }).join(',') + ')';
        layer.appendChild(s);
        return { el: s, cx: (t[0][0] + t[1][0] + t[2][0]) / 300, cy: (t[0][1] + t[1][1] + t[2][1]) / 300 };
      });
      el.appendChild(layer);
      return { el: el, shards: shards, broken: false, busy: 0, quietUntil: 0 };
    }

    // Burst away from the point (px, py), given as fractions of the box.
    function shatter(o, px, py) {
      if (o.broken) return;
      o.broken = true;
      clearTimeout(o.back);
      var w = o.el.offsetWidth, h = o.el.offsetHeight;
      var force = +o.el.dataset.force || DEFAULTS.force;
      var reach = Math.min(110, Math.max(w, h) * 0.22) * force;
      o.el.classList.add('lsh-broken');
      o.shards.forEach(function (s) {
        var dx = (s.cx - px) * w, dy = (s.cy - py) * h;
        var dist = Math.sqrt(dx * dx + dy * dy) || 1;
        // Shards near the point of impact fly furthest.
        var push = reach * (0.55 + 0.9 * Math.max(0, 1 - dist / Math.max(w, h))) * rnd(0.75, 1.25);
        anime.utils.remove(s.el);
        anime.animate(s.el, {
          x: (dx / dist) * push + rnd(-6, 6),
          y: (dy / dist) * push * 0.8 + rnd(-6, 10),
          rotate: rnd(-50, 50),
          scale: rnd(0.82, 0.96),
          opacity: rnd(0.7, 0.95),
          duration: rnd(520, 760),
          ease: 'out(4)'
        });
      });
    }

    function mend(o) {
      if (!o.broken) return;
      o.broken = false;
      var last = 0;
      o.shards.forEach(function (s) {
        var delay = rnd(0, 120), dur = rnd(560, 720);
        last = Math.max(last, delay + dur);
        anime.utils.remove(s.el);
        anime.animate(s.el, { x: 0, y: 0, rotate: 0, scale: 1, opacity: 1, delay: delay, duration: dur, ease: 'inOut(3)' });
      });
      clearTimeout(o.back);
      o.back = setTimeout(function () { if (!o.broken) o.el.classList.remove('lsh-broken'); }, last + 20);
    }

    function point(o, e) {
      var r = o.el.getBoundingClientRect();
      return [(e.clientX - r.left) / (r.width || 1), (e.clientY - r.top) / (r.height || 1)];
    }

    function once(o, px, py, hold) {
      shatter(o, px, py);
      clearTimeout(o.mendTimer);
      o.mendTimer = setTimeout(function () { mend(o); }, hold || DEFAULTS.hold);
    }

    function bind(o) {
      var el = o.el;
      var hover = window.matchMedia('(hover: hover) and (pointer: fine)');
      el.addEventListener('pointerenter', function (e) {
        if (e.pointerType !== 'mouse' || !hover.matches) return;
        o.quietUntil = Date.now() + 6000;
        clearTimeout(o.mendTimer);
        var p = point(o, e);
        shatter(o, p[0], p[1]);
      });
      el.addEventListener('pointerleave', function (e) {
        if (e.pointerType !== 'mouse' || !hover.matches) return;
        o.quietUntil = Date.now() + 4000;
        mend(o);
      });
      el.addEventListener('pointerdown', function (e) {
        if (e.pointerType === 'mouse' && hover.matches) return;
        o.quietUntil = Date.now() + 6000;
        var p = point(o, e);
        once(o, p[0], p[1], 1000);
      });
    }

    function autoplay(o) {
      var visible = false;
      var tick = function () {
        if (visible && !o.broken && Date.now() > o.quietUntil) once(o, rnd(0.3, 0.7), rnd(0.3, 0.7));
      };
      new IntersectionObserver(function (entries) { visible = entries[0].isIntersecting; }).observe(o.el);
      setTimeout(tick, 900);
      setInterval(tick, DEFAULTS.every);
    }

    function init() {
      document.querySelectorAll('[data-logo-shatter-hover]').forEach(function (el) {
        if (el._lsh) return;
        var o = (el._lsh = setup(el));
        if (reduced()) return;
        bind(o);
        if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false') autoplay(o);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.logoShatter = function (el) {
      if (el && el._lsh && !reduced()) once(el._lsh, 0.5, 0.5);
    };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<a href="/" class="logo" data-logo-shatter-hover>
  <img src="/logo.svg" alt="Your brand" width="180">
</a>