← Backgrounds

Gradient Blinds

Tall slats of gradient colour hang behind a section and sway on their hinges like blinds in a breeze, a wave rolling along them, while a soft light drifts back and forth and catches each slat as it turns.

Let the light in

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-gradient-blinds to a section with a dark background, which shows between the slats.

Options

Add any of these attributes to the same element.

data-colorsComma-separated colours, spread from one end of the blinds to the other. Default violet, pink and amber.
data-sizeSlat width in px. Default 40.
data-angleHow far the slats swing, in degrees. Default 55.
data-speedSpeed multiplier. Default 1.
data-horizontalSlats run across, like venetian blinds, instead of hanging down.

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

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Gradient Blinds: slats of gradient colour sway on their hinges like
    // window blinds, a wave running along them, while a soft light drifts back
    // and forth and catches each slat as it turns. Drawn on a transparent canvas
    // behind your content, and paused when off screen.
    // Usage: <section data-gradient-blinds style="background:#140b24">…</section>
    // Optional: data-colors (comma-separated, spread from one end to the other,
    // default violet, pink and amber), data-size (slat width in px, default 40),
    // data-angle (how far the slats swing, in degrees, default 55),
    // data-speed (default 1), data-horizontal (slats run across, like
    // venetian blinds, instead of hanging down).
    var DEFAULTS = { colors: '#7c3aed,#db2777,#f59e0b', size: 40, angle: 55, speed: 1 };

    var css =
      '[data-gradient-blinds]{position:relative;isolation:isolate;overflow:hidden}' +
      '.gbl-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);

    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);
    }

    // The palette colour at position f (0 to 1) along the blinds.
    function along(pal, f) {
      if (pal.length === 1) return pal[0];
      var x = Math.max(0, Math.min(0.9999, f)) * (pal.length - 1);
      var i = Math.floor(x), k = x - i, a = pal[i], b = pal[i + 1];
      return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k];
    }

    // Darken towards black when b is low; lift towards white when b is high.
    function shade(c, b) {
      var dark = Math.max(0, 1 - b) * 0.7;
      var lift = Math.max(0, b - 0.82) * 2.2;
      return 'rgb(' + c.map(function (v) {
        v = v * (1 - dark);
        return Math.round(v + (255 - v) * lift * 0.55);
      }).join(',') + ')';
    }

    function build(el) {
      var d = el.dataset;
      var pal = (d.colors || DEFAULTS.colors).split(',').map(rgb);
      var size = +d.size || DEFAULTS.size;
      var swing = ((d.angle !== undefined ? +d.angle : DEFAULTS.angle) * Math.PI) / 180;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var horizontal = d.horizontal !== undefined && d.horizontal !== 'false';
      var canvas = document.createElement('canvas');
      canvas.className = 'gbl-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var w = 0, h = 0, dpr = 1, t = 7;
      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;
        draw();
      }

      function draw() {
        // Work in "across" (L) and "down" (M) so one loop draws both ways.
        var L = horizontal ? h : w, M = horizontal ? w : h;
        if (horizontal) ctx.setTransform(0, dpr, dpr, 0, 0, 0);
        else ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, L, M);
        var n = Math.max(4, Math.ceil(L / size));
        var pitch = L / n;
        var light = L * (0.5 + 0.42 * Math.sin(t * 0.27 * speed));
        var depth = Math.max(L, M) * 0.45;
        var sway = 0.55 + 0.45 * Math.sin(t * 0.19 * speed);
        for (var i = 0; i < n; i++) {
          var cx = (i + 0.5) * pitch;
          // A travelling wave turns each slat a little after its neighbour.
          var th = swing * sway * Math.sin(t * 0.8 * speed - i * 0.42);
          var half = Math.max(1, (pitch * 0.94 * Math.abs(Math.cos(th))) / 2);
          // How squarely this slat faces the light.
          var face = Math.cos(th - Math.atan2(light - cx, depth));
          var b = 0.35 + 0.65 * Math.max(0, face);
          var base = along(pal, cx / L);
          var g = ctx.createLinearGradient(cx - half, 0, cx + half, 0);
          var lean = th > 0 ? 1 : -1;
          g.addColorStop(0, shade(base, b * (1 - 0.18 * lean)));
          g.addColorStop(0.5, shade(base, b));
          g.addColorStop(1, shade(base, b * (1 + 0.18 * lean)));
          ctx.fillStyle = g;
          ctx.fillRect(cx - half, 0, half * 2, M);
        }
        // A soft pool of light where the lamp is, and a darker far end.
        var glow = ctx.createRadialGradient(light, M * 0.35, 0, light, M * 0.35, Math.max(L, M) * 0.55);
        glow.addColorStop(0, 'rgba(255,255,255,0.16)');
        glow.addColorStop(1, 'rgba(255,255,255,0)');
        ctx.fillStyle = glow;
        ctx.fillRect(0, 0, L, M);
        var fall = ctx.createLinearGradient(0, 0, 0, M);
        fall.addColorStop(0, 'rgba(0,0,0,0)');
        fall.addColorStop(1, 'rgba(0,0,0,0.28)');
        ctx.fillStyle = fall;
        ctx.fillRect(0, 0, L, M);
      }

      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();
          t += (last ? Math.min(50, now - last) : 16) / 1000;
          last = now;
          draw();
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

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

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-gradient-blinds style="background: #140b24; color: #fff">
  <h1>Let the light in</h1>
</section>