← Loaders

Pixel Snake

A little snake of lit pixels slithers round a small grid, its head brightest and its tail fading out behind it, like a retro handheld game waiting to start. Send it round every pixel in a loop, spiralling in and out, or zigzagging along the rows. 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-pixel-snake> where the loader should appear.

Options

Add any of these attributes to the same element.

data-sizePixels along each side, 3 to 8. Default 4. The loop needs an even number, so odd sizes go up by one.
data-lengthPixels in the snake. Default 4.
data-speedTime for each step. Default 110 ms.
data-pathloop (round every pixel and back to the start), spiral (in to the middle and out again) or zigzag (along the rows and back). Default loop.
data-headA different colour for the snake’s head.

Each pixel is 0.42em with a 0.12em gap, so the default grid is about 2em square. It only moves while it’s on screen. With reduced motion the snake sits still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Pixel Snake: a little snake of lit pixels slithers round a small grid,
    // its head brightest and its tail fading out behind it, like a retro
    // handheld game waiting to start.
    // Sized with font-size (each pixel is 0.42em), coloured with color.
    // It only moves while it's on screen.
    // Usage: <div data-pixel-snake aria-label="Loading"></div>
    // Optional: data-size (pixels along each side, 3 to 8, default 4),
    // data-length (pixels in the snake, default 4), data-speed (ms per step,
    // default 110), data-path ("loop" round every pixel and back to the start,
    // "spiral" in to the middle and out again, or "zigzag" along the rows and
    // back; default "loop"), data-head (a different colour for the head).
    var DEFAULTS = { size: 4, length: 4, speed: 110, path: 'loop' };

    var css =
      '.pxn{display:inline-grid;gap:.12em;vertical-align:middle}' +
      '.pxn-px{display:block;width:.42em;height:.42em;border-radius:.09em;background:currentColor;opacity:.12;transform:scale(.72);transition-property:opacity,transform,background-color;transition-timing-function:ease-out}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    // A loop through every cell that ends next to where it began. Needs an even size.
    function loopPath(n) {
      var p = [], r, c;
      for (c = 0; c < n; c++) p.push([0, c]);
      for (r = 1; r < n; r++) {
        if (r % 2) for (c = n - 1; c >= 1; c--) p.push([r, c]);
        else for (c = 1; c < n; c++) p.push([r, c]);
      }
      for (r = n - 1; r >= 1; r--) p.push([r, 0]);
      return p;
    }

    function spiralPath(n) {
      var p = [], top = 0, left = 0, bottom = n - 1, right = n - 1, i;
      while (top <= bottom && left <= right) {
        for (i = left; i <= right; i++) p.push([top, i]);
        for (i = top + 1; i <= bottom; i++) p.push([i, right]);
        if (top < bottom) for (i = right - 1; i >= left; i--) p.push([bottom, i]);
        if (left < right) for (i = bottom - 1; i > top; i--) p.push([i, left]);
        top++; left++; bottom--; right--;
      }
      return p;
    }

    function zigzagPath(n) {
      var p = [];
      for (var r = 0; r < n; r++) for (var c = 0; c < n; c++) p.push([r, r % 2 ? n - 1 - c : c]);
      return p;
    }

    // Ping-pong a one-way path so the snake turns round at each end.
    function bounce(p) { return p.concat(p.slice(1, -1).reverse()); }

    function build(el) {
      var d = el.dataset;
      var kind = d.path || DEFAULTS.path;
      var n = Math.max(3, Math.min(8, Math.round(+d.size) || DEFAULTS.size));
      if (kind === 'loop' && n % 2) n++;
      var len = Math.max(2, Math.min(n * n - 1, Math.round(+d.length) || DEFAULTS.length));
      var speed = Math.max(40, +d.speed || DEFAULTS.speed);
      var path = kind === 'spiral' ? bounce(spiralPath(n)) : kind === 'zigzag' ? bounce(zigzagPath(n)) : loopPath(n);

      el.classList.add('pxn');
      if (!el.getAttribute('role')) el.setAttribute('role', 'progressbar');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
      el.textContent = '';
      el.style.gridTemplateColumns = 'repeat(' + n + ', auto)';
      var cells = [];
      for (var i = 0; i < n * n; i++) {
        var s = document.createElement('span');
        s.className = 'pxn-px';
        s.setAttribute('aria-hidden', 'true');
        s.style.transitionDuration = Math.round(speed * 1.6) + 'ms';
        el.appendChild(s);
        cells.push(s);
      }

      var lit = [];
      function draw(step) {
        var level = {};
        for (var k = 0; k < len; k++) {
          var at = path[((step - k) % path.length + path.length) % path.length];
          var idx = at[0] * n + at[1];
          var v = 1 - k / len;
          if (!(idx in level) || level[idx] < v) level[idx] = v;
        }
        lit.forEach(function (idx) { if (!(idx in level)) paint(idx, 0, false); });
        lit = Object.keys(level).map(Number);
        var head = path[((step % path.length) + path.length) % path.length];
        lit.forEach(function (idx) { paint(idx, level[idx], idx === head[0] * n + head[1]); });
      }
      function paint(idx, v, isHead) {
        var c = cells[idx];
        c.style.opacity = v ? (0.18 + 0.82 * v).toFixed(2) : '';
        c.style.transform = v ? 'scale(' + (0.78 + 0.22 * v).toFixed(2) + ')' : '';
        c.style.backgroundColor = isHead && d.head ? d.head : '';
      }

      var step = len - 1;
      draw(step);
      if (reduced()) return null;
      var acc = 0, last = 0;
      return anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          acc += last ? Math.min(200, now - last) : 0;
          last = now;
          while (acc >= speed) { acc -= speed; step++; draw(step); }
        },
        onPause: function () { last = 0; },
      });
    }

    function init() {
      document.querySelectorAll('[data-pixel-snake]').forEach(function (el) {
        if (el._pxn) return;
        el._pxn = 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-pixel-snake style="font-size: 24px; color: #6a0dad" aria-label="Loading"></div>

<!-- a longer snake spiralling round a bigger grid: -->
<div data-pixel-snake data-path="spiral" data-size="5" data-length="7"></div>