← Backgrounds

Gradient Mesh Drift

A smooth mesh gradient fills a section, its colour points slowly wandering and trading colours, so the whole surface keeps shifting like the glossy backgrounds on modern product sites. Add a fine film grain for extra polish.

Your hero section

How to use it

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

Options

Add any of these attributes to the same element.

data-colorsComma-separated colours, any number. Default violet, pink, amber, sky blue and indigo.
data-speedSeconds per drift. Default 7.
data-grainA fine film grain over the colours.

It’s drawn on a tiny canvas and softened, so it costs very little, and it pauses when off screen. With reduced motion it shows one still gradient.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Gradient Mesh Drift: a smooth mesh gradient fills a section, its colour
    // points slowly wandering and trading colours, so the whole surface keeps
    // shifting like the backgrounds on modern product sites. Drawn small on a
    // canvas and softened, so it costs very little. Pauses when off screen.
    // Usage: <section data-gradient-mesh-drift>…</section>
    // Optional: data-colors (comma-separated, default violet, pink, amber,
    // sky blue and indigo), data-speed (seconds per drift, default 7),
    // data-grain (a fine film grain over the top).
    var DEFAULTS = { colors: ['#a855f7', '#ec4899', '#fbbf24', '#38bdf8', '#6366f1'], speed: 7 };
    var COLS = 3, ROWS = 3, RES = 56;
    var GRAIN = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'160\' height=\'160\'%3E%3Cfilter id=\'n\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'.9\' numOctaves=\'2\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23n)\'/%3E%3C/svg%3E")';

    var css =
      '[data-gradient-mesh-drift]{position:relative;isolation:isolate;overflow:hidden}' +
      '.gm-canvas{position:absolute;inset:-12%;width:124%;height:124%;z-index:-1;pointer-events:none;filter:blur(28px) saturate(1.25)}' +
      '.gm-grain{position:absolute;inset:0;z-index:-1;pointer-events:none;opacity:.22;mix-blend-mode:overlay;background-image:' + GRAIN + '}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // Any CSS colour to [r, g, b], via a canvas.
    var probe = document.createElement('canvas').getContext('2d');
    function rgb(c) {
      probe.fillStyle = '#000';
      probe.fillStyle = c;
      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 colors = (d.colors ? d.colors.split(',') : DEFAULTS.colors).map(function (c) { return rgb(c.trim()); });
      var speed = (+d.speed || DEFAULTS.speed) * 1000;
      var canvas = document.createElement('canvas');
      canvas.className = 'gm-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      if (d.grain !== undefined && d.grain !== 'false') {
        var grain = document.createElement('div');
        grain.className = 'gm-grain';
        grain.setAttribute('aria-hidden', 'true');
        canvas.after(grain);
      }
      var ctx = canvas.getContext('2d');
      var R = anime.utils.random;
      var w = RES, h = RES, img;

      // A loose grid of colour points, each with a home to wander around.
      var points = [];
      for (var j = 0; j < ROWS; j++) {
        for (var i = 0; i < COLS; i++) {
          var c = colors[(j * COLS + i * 2) % colors.length];
          points.push({ hx: i / (COLS - 1), hy: j / (ROWS - 1), x: i / (COLS - 1), y: j / (ROWS - 1), r: c[0], g: c[1], b: c[2], k: j * COLS + i });
        }
      }

      // The canvas is tiny (it's blurred anyway); it only follows the shape.
      function resize() {
        var ratio = el.clientHeight / Math.max(1, el.clientWidth);
        w = RES;
        h = Math.max(16, Math.round(RES * Math.min(2, Math.max(0.3, ratio))));
        canvas.width = w;
        canvas.height = h;
        img = ctx.createImageData(w, h);
        draw();
      }

      function draw() {
        var data = img.data, n = points.length, s2 = 0.07;
        for (var y = 0; y < h; y++) {
          var v = y / (h - 1);
          for (var x = 0; x < w; x++) {
            var u = x / (w - 1), r = 0, g = 0, b = 0, sum = 0;
            // Each point's pull falls off smoothly with distance.
            for (var k = 0; k < n; k++) {
              var p = points[k], dx = u - p.x, dy = v - p.y;
              var wt = Math.exp(-(dx * dx + dy * dy) / s2);
              r += p.r * wt; g += p.g * wt; b += p.b * wt; sum += wt;
            }
            var o = (y * w + x) * 4;
            data[o] = r / sum; data[o + 1] = g / sum; data[o + 2] = b / sum; data[o + 3] = 255;
          }
        }
        ctx.putImageData(img, 0, 0);
      }
      resize();
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

      var anims = [];
      points.forEach(function (p, i) {
        var step = 0;
        (function drift() {
          // Wander about its home, blending towards the next colour in the list.
          step++;
          var c = colors[(p.k * 2 + step) % colors.length];
          anims[i] = anime.animate(p, {
            x: p.hx + R(-0.32, 0.32, 3),
            y: p.hy + R(-0.32, 0.32, 3),
            r: c[0], g: c[1], b: c[2],
            duration: speed * R(0.75, 1.25, 2),
            ease: 'inOut(2)',
            onComplete: drift
          });
        })();
      });
      // Redraw every other frame: the change is slow and the result blurred.
      var frame = 0;
      var timer = anime.createTimer({ loop: true, duration: 1000, onUpdate: function () { if (frame++ % 2 === 0) draw(); } });
      new IntersectionObserver(function (entries) {
        var on = entries[0].isIntersecting;
        anims.concat(timer).forEach(function (a) { if (on) a.play(); else a.pause(); });
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-gradient-mesh-drift]').forEach(function (el) {
        if (el._gm) return;
        el._gm = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section class="hero" data-gradient-mesh-drift data-grain data-colors="#a855f7,#ec4899,#fbbf24,#38bdf8">
  <h1>Hello</h1>
</section>