← Backgrounds

Flickering Grid

A grid of small squares sits behind a section, each one flicking to a new brightness at random moments, so the whole surface twinkles quietly like a field of pixels. The edges fade away so it frames your content.

Built different

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-flickering-grid to a section, hero or card.

Options

Add any of these attributes to the same element.

data-colorSquare colour. Default violet #a855f7.
data-sizeSquare size in px. Default 6.
data-gapSpace between squares in px. Default 4.
data-opacityThe brightest a square gets, from 0 to 1. Default 0.5.
data-rateHow often each square flickers, per second. Default 0.6.
data-fade="false"No fade at the edges.

It works on light backgrounds too: try a dark colour with a low opacity, e.g. data-color="#000" data-opacity="0.15". It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion the squares hold still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Flickering Grid: a grid of small squares behind a section, each one
    // flicking to a new brightness at random moments, so the whole surface
    // twinkles quietly like a field of pixels. The edges fade out. Drawn on a
    // transparent canvas behind your content, and paused when off screen.
    // Usage: <section data-flickering-grid>…</section>
    // Optional: data-color (default violet #a855f7), data-size (square size in
    // px, default 6), data-gap (space between squares in px, default 4),
    // data-opacity (brightest a square gets, default 0.5), data-rate (how
    // often each square flickers, per second, default 0.6), data-fade="false"
    // (no fade at the edges).
    var DEFAULTS = { color: '#a855f7', size: 6, gap: 4, opacity: 0.5, rate: 0.6 };

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

    function build(el) {
      var d = el.dataset;
      var color = d.color || DEFAULTS.color;
      var size = +d.size || DEFAULTS.size;
      var gap = d.gap !== undefined ? +d.gap : DEFAULTS.gap;
      var maxOp = d.opacity !== undefined ? +d.opacity : DEFAULTS.opacity;
      var rate = d.rate !== undefined ? +d.rate : DEFAULTS.rate;
      var canvas = document.createElement('canvas');
      canvas.className = 'flg-canvas' + (d.fade === 'false' ? '' : ' flg-fade');
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var w = 0, h = 0, dpr = 1, cols = 0, rows = 0, ox = 0, oy = 0, ops = null;
      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;
        var pitch = size + gap;
        cols = Math.ceil(w / pitch) + 1;
        rows = Math.ceil(h / pitch) + 1;
        // Centre the grid, so the margins are even on both sides.
        ox = (w - (cols * pitch - gap)) / 2;
        oy = (h - (rows * pitch - gap)) / 2;
        ops = new Float32Array(cols * rows);
        for (var i = 0; i < ops.length; i++) ops[i] = Math.random() * maxOp;
        draw();
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        ctx.fillStyle = color;
        var pitch = size + gap;
        for (var r = 0; r < rows; r++) {
          for (var c = 0; c < cols; c++) {
            var a = ops[r * cols + c];
            if (a < 0.01) continue;
            ctx.globalAlpha = a;
            ctx.fillRect(ox + c * pitch, oy + r * pitch, size, size);
          }
        }
        ctx.globalAlpha = 1;
      }

      // Each square has a small chance every frame of jumping to a new
      // brightness; most of the time it stays put.
      function advance(dt) {
        var chance = (rate * dt) / 1000;
        for (var i = 0; i < ops.length; i++) {
          if (Math.random() < chance) ops[i] = Math.random() * maxOp;
        }
      }

      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-flickering-grid]').forEach(function (el) {
        if (el._flg) return;
        el._flg = true;
        build(el);
      });
    }

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