← SVG & icons

Burst Lines

Short cartoon lines shoot out all round an element and vanish, the way a comic shows a “pow!” or a sudden surprise, while the element swells for a moment. It bursts when scrolled into view, when tapped or clicked, and when the mouse comes over it.

Wow!

Tap or hover either one to burst it

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-burst-lines to any element: a word, an icon, a button.
  3. Add data-loop to burst again every so often, or call animacious.burstLines(el) to burst it yourself, say when something is saved.

Options

Add any of these attributes to the same element.

data-loopBurst again every data-interval while on screen.
data-intervalTime between bursts, when looping. Default 1800 ms.
data-countNumber of lines. Default 10.
data-lengthLength of the longer lines. Default 16 px.
data-gapSpace between the element and the lines. Default 8 px.
data-widthLine width. Default 3 px.
data-colorLine colour, or several separated by commas to take turns. Default the text colour.
data-durationHow long a burst lasts. Default 520 ms.
data-popHow much the element swells. Default 0.08; 0 for none.
data-roundArrange the lines on a circle, for round icons and avatars.

The swell uses the element’s transform, so use it on an inline-block or block element without a transform of its own. The lines are hidden from screen readers, and nothing moves with reduced motion.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Burst Lines: short cartoon lines shoot out all round an element for
    // emphasis, the way a comic draws a "pow!" or a sudden surprise. It bursts
    // when the element scrolls into view, when it's clicked or tapped, and when
    // the mouse comes over it.
    // Usage: <span data-burst-lines>Wow!</span>
    // Optional: data-loop (burst again every data-interval ms, default 1800),
    // data-count (lines, default 10), data-length (px, default 16),
    // data-gap (space between the element and the lines, px, default 8),
    // data-width (line width, px, default 3), data-color (one colour, or several
    // separated by commas to take turns; default the text colour),
    // data-duration (ms, default 520), data-pop (how much the element swells,
    // default 0.08; 0 for none), data-round (lines sit on a circle rather than
    // round a box: for round icons and avatars).
    // Script use: animacious.burstLines(el)
    var DEFAULTS = { interval: 1800, count: 10, length: 16, gap: 8, width: 3, duration: 520, pop: 0.08 };
    var SVGNS = 'http://www.w3.org/2000/svg';

    var css = '.bsl-svg{position:absolute;pointer-events:none;overflow:visible;max-width:none;max-height:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function num(v, f) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : f; }
    function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
    function easeIn(t) { return t * t; }
    function clamp(t) { return t < 0 ? 0 : t > 1 ? 1 : t; }

    function setup(el) {
      if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
      var svg = document.createElementNS(SVGNS, 'svg');
      svg.setAttribute('class', 'bsl-svg');
      svg.setAttribute('aria-hidden', 'true');
      svg.setAttribute('fill', 'none');
      svg.setAttribute('stroke-linecap', 'round');
      el.appendChild(svg);
      el._bslSvg = svg;
    }

    function burst(el) {
      if (!el._bslSvg) return;
      var d = el.dataset, svg = el._bslSvg;
      var count = Math.max(3, num(d.count, DEFAULTS.count));
      var len = num(d.length, DEFAULTS.length);
      var gap = num(d.gap, DEFAULTS.gap);
      var width = num(d.width, DEFAULTS.width);
      var dur = num(d.duration, DEFAULTS.duration);
      var pop = num(d.pop, DEFAULTS.pop);
      var colors = (d.color || 'currentColor').split(',');
      var round = d.round !== undefined && d.round !== 'false';

      // Size the overlay to the element plus room for the lines.
      var w = el.offsetWidth, h = el.offsetHeight, m = gap + len * 1.5 + width;
      svg.style.left = svg.style.top = -m + 'px';
      svg.style.width = (w + 2 * m) + 'px';
      svg.style.height = (h + 2 * m) + 'px';
      svg.setAttribute('viewBox', '0 0 ' + (w + 2 * m) + ' ' + (h + 2 * m));
      svg.textContent = '';
      var cx = m + w / 2, cy = m + h / 2;
      var a = w / 2 + gap, b = h / 2 + gap;
      if (round) a = b = Math.max(w, h) / 2 + gap;
      // A squarish curve round a box (a superellipse) and a circle round a round thing.
      var n = round ? 2 : 5;
      var spin = Math.random() * Math.PI * 2, stepA = Math.PI * 2 / count;

      var lines = [];
      for (var i = 0; i < count; i++) {
        var th = spin + i * stepA + (Math.random() - 0.5) * stepA * 0.35;
        var c = Math.cos(th), s = Math.sin(th);
        var r = Math.pow(Math.pow(Math.abs(c / a), n) + Math.pow(Math.abs(s / b), n), -1 / n);
        // Long and short lines take turns, each a little different.
        var L = len * (i % 2 ? 0.62 : 1) * (0.85 + Math.random() * 0.3);
        var ln = document.createElementNS(SVGNS, 'line');
        ln.setAttribute('stroke', colors[i % colors.length].trim());
        ln.setAttribute('stroke-width', width);
        svg.appendChild(ln);
        lines.push({ el: ln, x: cx + c * r, y: cy + s * r, dx: c, dy: s, L: L, wait: Math.random() * dur * 0.12 });
      }

      if (pop > 0 && window.anime) {
        anime.animate(el, { scale: [1, 1 + pop, 1], duration: dur * 0.9, ease: 'out(3)' });
      }

      var start = 0;
      cancelAnimationFrame(el._bslRaf);
      function frame(now) {
        if (!start) start = now;
        var done = true;
        lines.forEach(function (o) {
          var t = clamp((now - start - o.wait) / dur);
          if (t < 1) done = false;
          // The head shoots out first, then the tail catches it up, while the
          // whole line drifts a little further out.
          var head = easeOut(clamp(t / 0.55)), tail = easeIn(clamp((t - 0.3) / 0.7));
          var drift = o.L * 0.45 * easeOut(t);
          var h1 = drift + o.L * head, t1 = drift + o.L * tail;
          o.el.setAttribute('x1', o.x + o.dx * t1);
          o.el.setAttribute('y1', o.y + o.dy * t1);
          o.el.setAttribute('x2', o.x + o.dx * h1);
          o.el.setAttribute('y2', o.y + o.dy * h1);
          o.el.setAttribute('opacity', t >= 1 || h1 - t1 < 0.3 ? 0 : 1);
        });
        el._bslRaf = done ? 0 : requestAnimationFrame(frame);
      }
      el._bslRaf = requestAnimationFrame(frame);
    }

    function init() {
      var still = reduced();
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var el = e.target;
          el._bslIn = e.isIntersecting;
          if (!e.isIntersecting) return;
          if (!el._bslSeen) { el._bslSeen = true; burst(el); }
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-burst-lines]').forEach(function (el) {
        if (el._bsl) return;
        el._bsl = true;
        if (still) return;
        setup(el);
        el.addEventListener('pointerdown', function () { el._bslTouch = Date.now(); burst(el); });
        el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') burst(el); });
        io.observe(el);
        var d = el.dataset;
        if (d.loop !== undefined && d.loop !== 'false') {
          var every = num(d.interval, DEFAULTS.interval);
          setInterval(function () {
            // Skip a beat after a real tap, and stay quiet off screen.
            if (el._bslIn && !document.hidden && Date.now() - (el._bslTouch || 0) > every) burst(el);
          }, every);
        }
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.burstLines = burst;

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<span data-burst-lines data-color="#8b5cf6,#ec4899"
      style="display: inline-block">Wow!</span>