← Backgrounds

Animated Grid

A glowing perspective grid scrolls towards you out of the horizon, the retro “synthwave” floor. Add a striped sunset sun for the full eighties look.

Back to the future

How to use it

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

Options

Add any of these attributes to the same element.

data-colorLine colour. Default #e879f9.
data-sizeCell width in px at the bottom edge. Default 64.
data-speedCells per second. Default 0.8.
data-horizonHow far down the horizon sits, from 0 (top) to 1 (bottom). Default 0.55.
data-sunA striped sunset sun on the horizon. Give two colours, e.g. data-sun="#fde047,#ec4899", to choose its gradient.

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

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Animated Grid: a glowing perspective grid scrolls towards you from the
    // horizon, the retro "synthwave" floor. Add a striped sunset sun if you
    // like. Drawn on a canvas behind your content, and paused when off screen.
    // Usage: <section data-animated-grid style="background:#12062b;color:#fff">…</section>
    // Optional: data-color (line colour, default #e879f9), data-size (cell
    // width in px at the bottom edge, default 64), data-speed (cells per
    // second, default 0.8), data-horizon (how far down the horizon sits, 0 to
    // 1, default 0.55), data-sun (a sunset sun on the horizon; give two
    // colours, e.g. data-sun="#fde047,#ec4899", to choose its gradient).
    var DEFAULTS = { color: '#e879f9', size: 64, speed: 0.8, horizon: 0.55, sun: ['#fde047', '#f472b6'] };

    var css =
      '[data-animated-grid]{position:relative;isolation:isolate;overflow:hidden}' +
      '.ag-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // Colour with alpha, via a canvas so any CSS colour works.
    var probe = document.createElement('canvas').getContext('2d');
    function alpha(c, a) {
      probe.fillStyle = '#000';
      probe.fillStyle = c;
      var v = probe.fillStyle;
      var rgb = v[0] === '#' ? [parseInt(v.slice(1, 3), 16), parseInt(v.slice(3, 5), 16), parseInt(v.slice(5, 7), 16)] : v.match(/[\d.]+/g).slice(0, 3);
      return 'rgba(' + rgb.join(',') + ',' + a + ')';
    }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'ag-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var color = d.color || DEFAULTS.color;
      var size = +d.size || DEFAULTS.size;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var horizon = Math.min(0.9, Math.max(0.1, +d.horizon || DEFAULTS.horizon));
      var sun = d.sun === undefined || d.sun === 'false' ? null : (d.sun.indexOf(',') > 0 ? d.sun.split(',') : DEFAULTS.sun);
      var w = 0, h = 0, dpr = 1, lineFade, glow;
      var state = { z: 0 };

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        var hy = h * horizon;
        // Lines fade out into the horizon.
        lineFade = ctx.createLinearGradient(0, hy, 0, h);
        lineFade.addColorStop(0, alpha(color, 0));
        lineFade.addColorStop(0.06, alpha(color, 0));
        lineFade.addColorStop(0.4, alpha(color, 0.6));
        lineFade.addColorStop(1, alpha(color, 1));
        glow = ctx.createLinearGradient(0, hy - h * 0.18, 0, hy + h * 0.12);
        glow.addColorStop(0, alpha(color, 0));
        glow.addColorStop(0.6, alpha(color, 0.28));
        glow.addColorStop(1, alpha(color, 0));
        draw();
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        var hy = h * horizon, depth = h - hy, cx = w / 2;
        if (sun) drawSun(cx, hy);
        ctx.fillStyle = glow;
        ctx.fillRect(0, hy - h * 0.18, w, h * 0.3);
        // A point on the floor at sideways X and distance z (1 = bottom edge).
        ctx.beginPath();
        var far = Math.min(80, Math.sqrt(depth / 1.2));
        var off = state.z % 1;
        for (var n = 0; n < far; n++) {
          var z = n + 1 - off;
          if (z < 0.6) continue;
          var y = hy + depth / z;
          ctx.moveTo(0, y);
          ctx.lineTo(w, y);
        }
        var reach = Math.ceil(w / 2 / size) * 4;
        for (var i = -reach; i <= reach; i++) {
          ctx.moveTo(cx + (i * size) / 0.6, hy + depth / 0.6);
          ctx.lineTo(cx + (i * size) / far, hy + depth / far);
        }
        // A soft wide pass for the glow, then the crisp line.
        ctx.strokeStyle = lineFade;
        ctx.globalAlpha = 0.22;
        ctx.lineWidth = 5;
        ctx.stroke();
        ctx.globalAlpha = 1;
        ctx.lineWidth = 1.3;
        ctx.stroke();
      }

      function drawSun(cx, hy) {
        var r = Math.min(w, h) * 0.2;
        var cy = hy - r * 0.35;
        ctx.save();
        ctx.beginPath();
        ctx.rect(0, 0, w, hy);
        ctx.clip();
        var g = ctx.createLinearGradient(0, cy - r, 0, cy + r);
        g.addColorStop(0, sun[0]);
        g.addColorStop(1, sun[1]);
        ctx.fillStyle = g;
        ctx.shadowColor = sun[1];
        ctx.shadowBlur = 40;
        ctx.beginPath();
        ctx.arc(cx, cy, r, 0, Math.PI * 2);
        ctx.fill();
        // Slits across its lower half, thickening and sliding down.
        ctx.globalCompositeOperation = 'destination-out';
        ctx.shadowBlur = 0;
        var band = r / 4.5, slide = (state.z * 0.35) % 1;
        for (var k = -1; k < 5; k++) {
          var top = cy + (k + slide) * band;
          var t = (top - cy) / r;
          if (t < 0) continue;
          ctx.fillRect(cx - r, top, r * 2, Math.max(0.5, band * t * 0.75));
        }
        ctx.restore();
      }

      resize();
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

      var anim = anime.animate(state, {
        z: [0, 100],
        duration: (100 / Math.max(0.01, speed)) * 1000,
        ease: 'linear',
        loop: true,
        autoplay: false,
        onUpdate: draw
      });
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) anim.play(); else anim.pause();
      }).observe(el);
    }

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

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-animated-grid data-sun style="background: linear-gradient(#0b0221, #3b0764 55%, #12062b 55%); color: #fff">
  <h1>Back to the future</h1>
</section>