← Backgrounds

Aurora Curtains

Shimmering curtains of light hang across a starry night sky, bright green along their lower hems and fading to violet above. They fold and ripple slowly, glowing brighter where they double back, while fine rays flicker along them.

Chase the lights

How to use it

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

Options

Add any of these attributes to the same element.

data-colorsTwo colours: the lower hem, then the top of the curtains. Default green #4ade80 and violet #a78bfa.
data-curtainsHow many curtains. Default 3.
data-speedSpeed multiplier. Default 1.
data-stars="false"No twinkling stars.

Looking for soft, blurred colour instead? See Aurora Gradient. This one is drawn on a transparent canvas behind your content and paused when off screen. With reduced motion it shows one still frame.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Aurora Curtains: shimmering curtains of light hang across a night sky,
    // bright green along their lower hem and fading to violet above. They
    // fold and ripple slowly, and fine rays flicker along them. Drawn on a
    // transparent canvas behind your content, and paused when off screen.
    // Usage: <section data-aurora-curtains style="background:#050816">…</section>
    // Optional: data-colors (two colours: the hem, then the top of the
    // curtains, default "#4ade80,#a78bfa"), data-curtains (how many, default
    // 3), data-speed (default 1), data-stars="false" (no stars).
    var DEFAULTS = { colors: '#4ade80,#a78bfa', curtains: 3, speed: 1 };

    var css =
      '[data-aurora-curtains]{position:relative;isolation:isolate;overflow:hidden}' +
      '.auc-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);
    }
    function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; }

    function build(el) {
      var d = el.dataset;
      var cols = (d.colors || DEFAULTS.colors).split(',');
      var hem = rgb(cols[0]), crown = rgb(cols[1] || cols[0]);
      var hemHot = hem.map(function (v) { return Math.round(v + (255 - v) * 0.55); });
      var count = Math.max(1, +d.curtains || DEFAULTS.curtains);
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var starry = d.stars !== 'false';
      var canvas = document.createElement('canvas');
      canvas.className = 'auc-canvas';
      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, t = 0, stars = [], grad = null;
      var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

      // Each curtain has its own height in the sky, its own folds and drift.
      var curtains = [];
      for (var i = 0; i < count; i++) {
        curtains.push({
          y: 0.42 + 0.28 * (count === 1 ? 0.5 : i / (count - 1)) + R(-0.04, 0.04, 3),
          tall: R(0.32, 0.5, 2),
          a: R(0.15, 0.3, 2), b: R(0.3, 0.55, 2), p1: R(0, 6.28, 2), p2: R(0, 6.28, 2),
          fold: R(0.08, 0.16, 3), fk: R(1.6, 3, 2), fs: R(0.12, 0.25, 2) * (i % 2 ? -1 : 1),
          power: R(0.55, 0.9, 2) * (i === 0 ? 1 : 0.85)
        });
      }

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        var n = Math.round(Math.min(220, (w * h) / 1800));
        stars = [];
        for (var i = 0; i < n; i++) stars.push({ x: R(0, w, 1), y: R(0, h, 1), r: R(0.3, 1.2, 2), p: R(0, 6.28, 2), s: R(0.5, 2, 2) });
        // One curtain-shaped gradient, from the top (0) to the hem (1),
        // stretched to fit each strip of light.
        grad = ctx.createLinearGradient(0, 0, 0, 1);
        grad.addColorStop(0, rgba(crown, 0));
        grad.addColorStop(0.35, rgba(crown, 0.35));
        grad.addColorStop(0.7, rgba(hem, 0.55));
        grad.addColorStop(0.93, rgba(hemHot, 0.95));
        grad.addColorStop(1, rgba(hem, 0));
        draw();
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.globalCompositeOperation = 'source-over';
        ctx.clearRect(0, 0, w, h);
        if (starry) {
          ctx.fillStyle = '#fff';
          for (var i = 0; i < stars.length; i++) {
            var s = stars[i];
            ctx.globalAlpha = 0.35 + 0.35 * Math.sin(t * s.s + s.p);
            ctx.fillRect(s.x, s.y, s.r * 1.6, s.r * 1.6);
          }
          ctx.globalAlpha = 1;
        }
        ctx.globalCompositeOperation = 'lighter';
        ctx.fillStyle = grad;
        var step = Math.max(2, w / 360);
        var unit = w / Math.PI;
        for (var c = 0; c < curtains.length; c++) {
          var C = curtains[c];
          for (var sx = -w * 0.15; sx < w * 1.15; sx += step) {
            var u = sx / unit;
            // Folding: the curtain sways sideways by different amounts along
            // its length; where it doubles back on itself it looks brighter.
            var phase = u * C.fk + t * C.fs + C.p2;
            var x = sx + Math.sin(phase) * C.fold * w * 0.8;
            var slope = 1 + Math.cos(phase) * C.fold * 0.8 * C.fk * Math.PI;
            var fold = Math.min(2.4, 1 / Math.max(0.25, Math.abs(slope)));
            // The hem waves up and down slowly.
            var hemY = h * (C.y + 0.07 * Math.sin(u * C.a * 4 + t * 0.21 + C.p1) + 0.04 * Math.sin(u * C.b * 6 - t * 0.33));
            var tall = h * C.tall * (0.75 + 0.25 * Math.sin(u * 2.3 + t * 0.4 + C.p1));
            // Shimmer: fine rays that flicker, and brighter patches that roam.
            var rays = 0.62 + 0.38 * Math.sin(sx * 0.35 + Math.sin(sx * 0.031 + t * 0.7) * 6 + t * 1.3);
            var roam = 0.45 + 0.55 * Math.pow(0.5 + 0.5 * Math.sin(u * 3.1 - t * 0.55 + C.p1 * 2), 2);
            var edge = Math.min(1, Math.min(sx + w * 0.15, w * 1.15 - sx) / (w * 0.2));
            var alpha = C.power * rays * roam * fold * 0.5 * edge;
            if (alpha < 0.01) continue;
            ctx.globalAlpha = Math.min(1, alpha);
            ctx.setTransform(dpr, 0, 0, dpr * tall, x * dpr, (hemY - tall) * dpr);
            ctx.fillRect(0, 0, step * 1.4, 1.06);
          }
        }
        ctx.globalAlpha = 1;
        ctx.globalCompositeOperation = 'source-over';
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      }

      t = R(0, 50);
      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;
          t += (dt / 1000) * speed;
          draw();
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-aurora-curtains]').forEach(function (el) {
        if (el._auc) return;
        el._auc = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-aurora-curtains style="background: linear-gradient(#030712, #0b1530); color: #fff">
  <h1>Chase the lights</h1>
</section>