← Loaders

Infinity Loader

A stroke chases itself round a figure-of-eight. The head runs on ahead while the tail catches up, so the line stretches and shrinks as it loops through the crossing. Add a faint track or a two-colour gradient. Size it with font-size and colour it with color.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-infinity-loader> where the loader should appear.

Options

Add any of these attributes to the same element.

data-speedTime for one lap of the figure-of-eight. Default 1600 ms.
data-trackShow a faint figure-of-eight behind the stroke. Give it a colour, or leave it empty for a faded copy of the stroke’s.
data-thicknessLine width, in em. Default 0.14.
data-colorsTwo comma-separated colours for a gradient stroke, or one to override color.

It’s 2.8em wide and 1.4em tall. It only moves while it’s on screen. With reduced motion it shows a still stretch of line.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Infinity Loader: a stroke chases itself round a figure-of-eight. Its head
    // runs ahead and its tail catches up, so the line grows and shrinks as it
    // loops. Sized with font-size (it's 2.8em wide), coloured with color.
    // It only moves while it's on screen.
    // Usage: <div data-infinity-loader aria-label="Loading"></div>
    // Optional: data-speed (ms for one lap, default 1600), data-track (a faint
    // figure-of-eight behind the stroke; give it a colour, or leave it empty for
    // a faded copy of the stroke's), data-thickness (line width in em, default
    // 0.14), data-colors (two comma-separated colours for a gradient stroke).
    var DEFAULTS = { speed: 1600, thickness: 0.14 };

    var css =
      '.ifl{display:inline-block;width:2.8em;height:1.4em;vertical-align:middle}' +
      '.ifl svg{display:block;width:100%;height:100%;overflow:visible}' +
      '.ifl path{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}' +
      '.ifl-track{opacity:.15}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var NS = 'http://www.w3.org/2000/svg';
    // A smooth figure-of-eight in a 100 x 50 box, crossing in the middle.
    var D = 'M50,25 C60,12 90,10 90,25 C90,40 60,38 50,25 C40,12 10,10 10,25 C10,40 40,38 50,25';
    var uid = 0;

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function path(cls, w) {
      var p = document.createElementNS(NS, 'path');
      p.setAttribute('class', cls);
      p.setAttribute('d', D);
      p.setAttribute('pathLength', '100');
      p.setAttribute('stroke-width', w);
      return p;
    }

    function build(el) {
      var d = el.dataset;
      // Line width: em to viewBox units (the box is 2.8em = 100 units wide).
      var w = Math.max(0.04, Math.min(0.4, +d.thickness || DEFAULTS.thickness)) * 100 / 2.8;
      el.classList.add('ifl');
      if (!el.getAttribute('role')) el.setAttribute('role', 'progressbar');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
      el.textContent = '';
      var svg = document.createElementNS(NS, 'svg');
      svg.setAttribute('viewBox', '0 0 100 50');
      svg.setAttribute('aria-hidden', 'true');
      if (d.track !== undefined && d.track !== 'false') {
        var track = path('ifl-track', w);
        if (d.track) { track.style.stroke = d.track; track.style.opacity = 1; }
        svg.appendChild(track);
      }
      var line = path('ifl-line', w);
      var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : [];
      if (colors.length > 1) {
        var id = 'ifl-grad-' + (++uid);
        var defs = document.createElementNS(NS, 'defs');
        defs.innerHTML = '<linearGradient id="' + id + '" x1="0" x2="1" y1="0" y2="0">' +
          '<stop offset="0" stop-color="' + colors[0] + '"/><stop offset="1" stop-color="' + colors[1] + '"/></linearGradient>';
        svg.appendChild(defs);
        line.style.stroke = 'url(#' + id + ')';
      } else if (colors.length) {
        line.style.stroke = colors[0];
      }
      svg.appendChild(line);
      el.appendChild(svg);

      function draw(t) {
        // The head moves steadily; the length swells and shrinks on a slower
        // beat than the lap, so it never quite repeats.
        var p = t / speed;
        var len = 18 + 28 * (0.5 - 0.5 * Math.cos(p * Math.PI * 2 / 1.5));
        line.setAttribute('stroke-dasharray', len + ' ' + (100 - len));
        line.setAttribute('stroke-dashoffset', (-p * 100 % 100) + len);
      }

      var speed = Math.max(300, +d.speed || DEFAULTS.speed);
      if (reduced()) { line.setAttribute('stroke-dasharray', '34 66'); line.setAttribute('stroke-dashoffset', '0'); return null; }
      draw(0);
      var t = 0, last = 0;
      return anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          t += last ? Math.min(50, now - last) : 16;
          last = now;
          draw(t);
        },
        onPause: function () { last = 0; },
      });
    }

    function init() {
      document.querySelectorAll('[data-infinity-loader]').forEach(function (el) {
        if (el._ifl) return;
        el._ifl = true;
        var timer = build(el);
        if (!timer) return;
        new IntersectionObserver(function (entries) {
          entries[0].isIntersecting ? timer.play() : timer.pause();
        }).observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-infinity-loader style="font-size: 24px; color: #6a0dad" aria-label="Loading"></div>

<!-- with a track and a gradient: -->
<div data-infinity-loader data-track data-colors="#a855f7,#ec4899"></div>