← SVG & icons

Speed Lines

An element zooms in from the side with comic speed lines streaking behind it, leaning into the dash. It skids to a stop, rocking back upright, and the lines snap in after it. Loop it and it winds up and zooms off the other side.

Express

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-speed-lines to the element that moves: a badge, an icon, a label.
  3. It travels across its parent, so give the parent overflow: hidden.

Options

Add any of these attributes to the same element.

data-loopRest, zoom off the far side, come back and repeat.
data-directionWhich way it travels: right or left. Default right.
data-holdHow long it rests before zooming off, when looping. Default 1600 ms.
data-durationTime for the dash in. Default 600 ms.
data-countNumber of lines. Default 6.
data-lengthLength of the longest line. Default 90 px.
data-colorLine colour. Default the text colour.
data-leanHow far it leans while moving. Default 14°.

The lines flicker to new lengths as it moves, like hand-drawn streaks. It pauses while off screen, and with reduced motion it simply sits still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Speed Lines: an element zooms in from the side with comic speed lines
    // streaking behind it, leaning into the dash. It skids to a stop and the lines
    // snap in after it. Loop it and it winds up and zooms off the other side,
    // then comes round again.
    // Usage: <div style="overflow: hidden"><span data-speed-lines>Express</span></div>
    // The element travels across its parent, so give the parent overflow: hidden.
    // Optional: data-loop (zoom off, come back, repeat), data-direction (right
    // or left: which way it travels, default right), data-hold (ms it rests
    // before zooming off, when looping, default 1600), data-count (lines,
    // default 6), data-length (longest line, px, default 90), data-color
    // (line colour, default the text colour), data-lean (degrees it leans
    // while moving, default 14), data-duration (ms for the dash in, default 600).
    var DEFAULTS = { hold: 1600, count: 6, length: 90, lean: 14, duration: 600 };

    var css = '.spdl-lines{position:absolute;top:0;bottom:0;width:0;pointer-events:none}' +
      '.spdl-line{position:absolute;height:3px;border-radius:3px;margin-top:-1.5px}';
    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 clamp(t) { return t < 0 ? 0 : t > 1 ? 1 : t; }
    function out(t, k) { return 1 - Math.pow(1 - t, k); }

    function setup(el) {
      var d = el.dataset;
      var dir = d.direction === 'left' ? -1 : 1;
      var count = Math.max(1, num(d.count, DEFAULTS.count));
      var maxLen = num(d.length, DEFAULTS.length);
      var color = d.color || 'currentColor';
      if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
      if (getComputedStyle(el).display === 'inline') el.style.display = 'inline-block';

      // The lines hang off the trailing side, so they travel with the element.
      var box = document.createElement('span');
      box.className = 'spdl-lines';
      box.setAttribute('aria-hidden', 'true');
      box.style[dir > 0 ? 'right' : 'left'] = '100%';
      var lines = [];
      for (var i = 0; i < count; i++) {
        var ln = document.createElement('span');
        ln.className = 'spdl-line';
        var top = 10 + (i + 0.5) / count * 80 + (Math.random() - 0.5) * (60 / count);
        ln.style.top = top.toFixed(1) + '%';
        ln.style.height = (i % 3 === 1 ? 2 : 3) + 'px';
        ln.style[dir > 0 ? 'right' : 'left'] = (6 + Math.random() * 12).toFixed(1) + 'px';
        ln.style.background = 'linear-gradient(' + (dir > 0 ? 'to left' : 'to right') + ', ' + color + ', transparent)';
        ln.style.transformOrigin = dir > 0 ? 'right center' : 'left center';
        box.appendChild(ln);
        lines.push({ el: ln, len: 0 });
      }
      el.appendChild(box);
      return { el: el, dir: dir, lines: lines, maxLen: maxLen };
    }

    // Where the element must sit to be just out of its parent on each side.
    function measure(o) {
      var el = o.el, p = el.parentElement;
      var keep = el.style.transform;
      el.style.transform = 'none';
      var r = el.getBoundingClientRect(), pr = p.getBoundingClientRect();
      el.style.transform = keep;
      var tail = o.maxLen + 30;
      if (o.dir > 0) {
        o.offIn = -(r.right - pr.left + 10);
        o.offOut = pr.right - r.left + tail;
      } else {
        o.offIn = pr.right - r.left + 10;
        o.offOut = -(r.right - pr.left + tail);
      }
    }

    function run(o) {
      var d = o.el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var E = num(d.duration, DEFAULTS.duration), S = 520, H = num(d.hold, DEFAULTS.hold), W = 240, X = 420, G = 300;
      var lean = num(d.lean, DEFAULTS.lean) * o.dir;
      var visible = false, raf = 0, last = 0, clock = 0, jitter = 0;
      measure(o);

      // Lines flicker to new lengths every few frames, like hand-drawn streaks.
      function lines(k, now) {
        var fresh = now - jitter > 70;
        if (fresh) jitter = now;
        o.lines.forEach(function (l) {
          if (fresh || !l.len) l.len = o.maxLen * (0.45 + Math.random() * 0.55);
          l.el.style.width = l.len.toFixed(1) + 'px';
          l.el.style.transform = 'scaleX(' + k.toFixed(3) + ')';
          l.el.style.opacity = k > 0.01 ? 1 : 0;
        });
      }

      function pose(x, skew) {
        o.el.style.transform = 'translateX(' + x.toFixed(1) + 'px) skewX(' + (-skew).toFixed(2) + 'deg)';
      }

      function frame(now) {
        raf = 0;
        if (!visible) return;
        clock += Math.min(50, now - (last || now));
        last = now;
        var cycle = E + S + H + W + X + G;
        var t = loop ? clock % cycle : Math.min(clock, E + S);
        var p;
        if (t < E) {
          p = t / E;
          pose(o.offIn * (1 - out(p, 4)), lean * out(clamp(p * 3), 2));
          lines(clamp(p * 5), now);
        } else if ((t -= E) < S) {
          // Skid: the lean springs back past upright and settles; the lines catch up.
          p = t / S;
          pose(0, lean * Math.exp(-4 * p) * Math.cos(Math.PI * 2.4 * p));
          lines(1 - out(clamp(p * 1.7), 2), now);
        } else if ((t -= S) < H) {
          pose(0, 0);
          lines(0, now);
        } else if ((t -= H) < W) {
          // Wind up: a little step back, leaning away.
          p = out(t / W, 2);
          pose(-12 * o.dir * p, -lean * 0.55 * p);
          lines(0, now);
        } else if ((t -= W) < X) {
          p = t / X;
          pose(-12 * o.dir + (o.offOut + 12 * o.dir) * Math.pow(p, 3), lean * (1.25 * out(clamp(p * 2.5), 2) - 0.55 * (1 - clamp(p * 2.5))));
          lines(clamp(p * 4), now);
        } else {
          pose(o.offIn, 0);
          lines(0, now);
        }
        if (loop || clock < E + S) raf = requestAnimationFrame(frame);
      }

      pose(o.offIn, 0);
      lines(0, 0);
      new IntersectionObserver(function (entries) {
        visible = entries[0].isIntersecting;
        last = 0;
        if (visible && !raf) raf = requestAnimationFrame(frame);
      }).observe(o.el.parentElement);
      if (window.ResizeObserver) new ResizeObserver(function () { measure(o); }).observe(o.el.parentElement);
    }

    function init() {
      if (reduced()) return;
      document.querySelectorAll('[data-speed-lines]').forEach(function (el) {
        if (el._spdl) return;
        el._spdl = true;
        run(setup(el));
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div style="overflow: hidden">
  <span data-speed-lines data-loop data-color="#8b5cf6">Express</span>
</div>