← Backgrounds

Grid Beams

A faint grid sits behind a section and thin beams of light shoot along its lines, each a bright head with a fading tail that lights up the crossings it passes. The edges fade away so the grid frames your content.

Ship faster

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-grid-beams to a section with a dark background.

Options

Add any of these attributes to the same element.

data-colorsBeam colours, comma-separated. Default violet, cyan and pink.
data-gridGrid line colour. Default rgba(255,255,255,.08).
data-sizeGrid cell size in px. Default 48.
data-rateBeams per second for a 600 × 360 section, scaled up or down with its area. Default 1.6.
data-speedSpeed multiplier. Default 1.
data-fade="false"No fade at the edges.

It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion a few beams hold still on the grid.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Grid Beams: a faint grid sits behind a section and thin beams of light
    // shoot along its lines, each one a bright head with a fading tail that
    // lights up the crossings it passes. The edges fade away so it frames your
    // content. Drawn on a transparent canvas, and paused when off screen.
    // Usage: <section data-grid-beams style="background:#0a0a14">…</section>
    // Optional: data-colors (beam colours, comma-separated, default violet,
    // cyan and pink), data-grid (grid line colour, default
    // rgba(255,255,255,.08)), data-size (cell size in px, default 48),
    // data-rate (beams per second for a 600 by 360 section, scaled by area,
    // default 1.6), data-speed (multiplier, default 1), data-fade="false"
    // (no fade at the edges).
    var DEFAULTS = { colors: '#a855f7,#22d3ee,#f472b6', grid: 'rgba(255,255,255,.08)', size: 48, rate: 1.6, speed: 1 };

    var css =
      '[data-grid-beams]{position:relative;isolation:isolate;overflow:hidden}' +
      '.gbm-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}' +
      '.gbm-fade{-webkit-mask-image:radial-gradient(ellipse 72% 72% at 50% 50%,#000 40%,transparent 100%);mask-image:radial-gradient(ellipse 72% 72% at 50% 50%,#000 40%,transparent 100%)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var TAU = Math.PI * 2;
    var probe = document.createElement('canvas').getContext('2d');
    function rgb(c) {
      probe.fillStyle = '#000';
      probe.fillStyle = c.trim();
      var v = probe.fillStyle;
      if (v[0] === '#') return [parseInt(v.slice(1, 3), 16), parseInt(v.slice(3, 5), 16), parseInt(v.slice(5, 7), 16)];
      return v.match(/[\d.]+/g).slice(0, 3).map(Number);
    }

    function build(el) {
      var d = el.dataset;
      var cols = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return rgb(c).join(','); });
      var gridCol = d.grid || DEFAULTS.grid;
      var size = +d.size || DEFAULTS.size;
      var rate = d.rate !== undefined ? +d.rate : DEFAULTS.rate;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var canvas = document.createElement('canvas');
      canvas.className = 'gbm-canvas' + (d.fade === 'false' ? '' : ' gbm-fade');
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var R = anime.utils.random;
      var w = 0, h = 0, dpr = 1, ox = 0, oy = 0, nx = 0, ny = 0, beams = [], due = 0;
      var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        // Centre the grid so a line runs through the middle both ways.
        ox = ((w / 2) % size);
        oy = ((h / 2) % size);
        nx = Math.ceil((w - ox) / size) + 1;
        ny = Math.ceil((h - oy) / size) + 1;
        beams = [];
        if (still) for (var i = 0; i < 6; i++) { spawn(); beams[i].p = R(0.3, 0.8, 2) * beams[i].span; }
        draw();
      }

      function spawn() {
        var across = Math.random() < w / (w + h); // horizontal beams on wide sections
        var line = Math.round(across ? oy + R(0, ny - 1) * size : ox + R(0, nx - 1) * size);
        var span = across ? w : h;
        beams.push({
          across: across,
          line: line,
          dir: Math.random() < 0.5 ? 1 : -1,
          span: span,
          p: -size * 3,
          len: size * R(20, 40) / 10,
          v: R(160, 300) * speed,
          col: cols[R(0, cols.length - 1)]
        });
      }

      // Where along its line the beam's head is, as x and y.
      function at(b, p) {
        var s = b.dir > 0 ? p : b.span - p;
        return b.across ? [s, b.line] : [b.line, s];
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        ctx.strokeStyle = gridCol;
        ctx.lineWidth = 1;
        ctx.beginPath();
        for (var i = 0; i < nx; i++) { var x = Math.round(ox + i * size) + 0.5; ctx.moveTo(x, 0); ctx.lineTo(x, h); }
        for (var j = 0; j < ny; j++) { var y = Math.round(oy + j * size) + 0.5; ctx.moveTo(0, y); ctx.lineTo(w, y); }
        ctx.stroke();

        ctx.globalCompositeOperation = 'lighter';
        ctx.lineCap = 'round';
        for (var k = 0; k < beams.length; k++) {
          var b = beams[k];
          var head = at(b, b.p), tail = at(b, b.p - b.len);
          var g = ctx.createLinearGradient(tail[0], tail[1], head[0], head[1]);
          g.addColorStop(0, 'rgba(' + b.col + ',0)');
          g.addColorStop(1, 'rgba(' + b.col + ',1)');
          ctx.strokeStyle = g;
          ctx.lineWidth = 1.6;
          ctx.beginPath();
          ctx.moveTo(tail[0] + 0.5, tail[1] + 0.5);
          ctx.lineTo(head[0] + 0.5, head[1] + 0.5);
          ctx.stroke();
          // A soft glow round the head, and the nearest crossing lights up.
          var glow = ctx.createRadialGradient(head[0], head[1], 0, head[0], head[1], 14);
          glow.addColorStop(0, 'rgba(' + b.col + ',0.7)');
          glow.addColorStop(1, 'rgba(' + b.col + ',0)');
          ctx.fillStyle = glow;
          ctx.beginPath();
          ctx.arc(head[0], head[1], 14, 0, TAU);
          ctx.fill();
          var cross = b.across ? head[0] - ox : head[1] - oy;
          var near = Math.abs(cross - Math.round(cross / size) * size);
          if (near < size * 0.3) {
            var cx = b.across ? ox + Math.round(cross / size) * size : b.line;
            var cy = b.across ? b.line : oy + Math.round(cross / size) * size;
            ctx.fillStyle = 'rgba(' + b.col + ',' + (0.5 * (1 - near / (size * 0.3))).toFixed(3) + ')';
            ctx.beginPath();
            ctx.arc(cx + 0.5, cy + 0.5, 2.5, 0, TAU);
            ctx.fill();
          }
          ctx.fillStyle = '#fff';
          ctx.beginPath();
          ctx.arc(head[0] + 0.5, head[1] + 0.5, 1.4, 0, TAU);
          ctx.fill();
        }
        ctx.globalCompositeOperation = 'source-over';
      }

      function advance(dt) {
        var s = dt / 1000;
        // More beams on bigger sections, so the grid feels equally busy.
        due -= s * rate * Math.max(0.4, (w * h) / (600 * 360));
        while (due <= 0) { spawn(); due += R(40, 160) / 100; }
        for (var i = beams.length - 1; i >= 0; i--) {
          var b = beams[i];
          b.p += b.v * s;
          if (b.p - b.len > b.span) beams.splice(i, 1);
        }
      }

      resize();
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
      if (still) return;

      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          advance(dt);
          draw();
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-grid-beams]').forEach(function (el) {
        if (el._gbm) return;
        el._gbm = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-grid-beams style="background: #0a0a14; color: #fff">
  <h1>Ship faster</h1>
</section>