← Backgrounds

Drifting Clouds

Soft, fluffy clouds drift across a blue sky behind a section. Big, near clouds glide past quickly while small, far ones crawl, so the sky has a gentle sense of depth.

Blue skies ahead

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-drifting-clouds to a section with a sky-coloured background.

Options

Add any of these attributes to the same element.

data-colorCloud colour. Default white.
data-shadeColour of the cloud undersides. Default blue-grey #94a3b8.
data-densityHow many clouds, as a multiplier. Default 1.
data-speedSpeed multiplier. Default 1.
data-direction="left"Drift to the left instead of the right.

Each cloud is painted once and then just slid along a transparent canvas, so it costs very little, and it pauses when off screen. With reduced motion the clouds hold still. For a sunset, try data-color="#fde2e4" data-shade="#9d4edd" on an orange sky.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Drifting Clouds: soft, fluffy clouds drift across a sky at different
    // speeds. Big, near clouds glide past quickly; small, far ones crawl. Drawn
    // on a transparent canvas behind your content, and paused when off screen.
    // Usage: <section data-drifting-clouds style="background:linear-gradient(#3b82f6,#bfdbfe)">…</section>
    // Optional: data-color (cloud colour, default white), data-shade (colour of
    // the cloud undersides, default blue-grey #94a3b8), data-density (how many
    // clouds, as a multiplier, default 1), data-speed (default 1),
    // data-direction="left" (drift to the left instead of the right).
    var DEFAULTS = { color: '#ffffff', shade: '#94a3b8', density: 1, speed: 1 };

    var css =
      '[data-drifting-clouds]{position:relative;isolation:isolate;overflow:hidden}' +
      '.dcl-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 TAU = Math.PI * 2;
    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 rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; }

    // Paints one cloud on its own small canvas: a row of soft puffs on a flat
    // base, with the underside shaded. Each cloud is drawn once and reused.
    function cloudSprite(ch, col, shade, scale) {
      var R = anime.utils.random;
      var cw = ch * 2.6;
      var c = document.createElement('canvas');
      c.width = Math.ceil(cw * scale);
      c.height = Math.ceil(ch * scale);
      var g = c.getContext('2d');
      g.scale(scale, scale);
      var base = ch * 0.78;
      function puff(x, y, r) {
        var gr = g.createRadialGradient(x, y, 0, x, y, r);
        gr.addColorStop(0, rgba(col, 1));
        gr.addColorStop(0.7, rgba(col, 0.9));
        gr.addColorStop(1, rgba(col, 0));
        g.fillStyle = gr;
        g.beginPath();
        g.arc(x, y, r, 0, TAU);
        g.fill();
      }
      var n = R(6, 9);
      for (var i = 0; i < n; i++) {
        var f = i / (n - 1);
        var mid = 1 - Math.abs(f - 0.5) * 2;
        var r = ch * (0.17 + 0.2 * mid + R(0, 0.06, 3));
        puff(cw * (0.22 + 0.56 * f) + R(-0.03, 0.03, 3) * cw, base - r * R(0.35, 0.7, 2), r);
      }
      for (var j = 0; j < n + 2; j++) {
        var rb = ch * R(0.15, 0.21, 3);
        puff(cw * (0.13 + 0.74 * j / (n + 1)), base - rb * 0.25, rb);
      }
      // A soft, flat bottom.
      g.globalCompositeOperation = 'destination-out';
      var cut = g.createLinearGradient(0, base - ch * 0.04, 0, base + ch * 0.12);
      cut.addColorStop(0, 'rgba(0,0,0,0)');
      cut.addColorStop(1, 'rgba(0,0,0,1)');
      g.fillStyle = cut;
      g.fillRect(0, base - ch * 0.04, cw, ch);
      // Shade the underside.
      g.globalCompositeOperation = 'source-atop';
      var sh = g.createLinearGradient(0, ch * 0.3, 0, base);
      sh.addColorStop(0, rgba(shade, 0));
      sh.addColorStop(1, rgba(shade, 0.6));
      g.fillStyle = sh;
      g.fillRect(0, 0, cw, ch);
      return { img: c, w: cw, h: ch };
    }

    function build(el) {
      var d = el.dataset;
      var col = rgb(d.color || DEFAULTS.color);
      var shade = rgb(d.shade || DEFAULTS.shade);
      var density = d.density !== undefined ? +d.density : DEFAULTS.density;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var dir = d.direction === 'left' ? -1 : 1;
      var canvas = document.createElement('canvas');
      canvas.className = 'dcl-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, clouds = [];
      var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

      // Three depths: far clouds are small, faint and slow; near ones big,
      // solid and quick.
      function makeClouds() {
        clouds = [];
        var unit = Math.max(120, Math.min(w, h * 1.6));
        [0.38, 0.64, 1].forEach(function (depth) {
          var ch = unit * 0.32 * depth;
          var n = Math.max(1, Math.round((w / (ch * 2.6)) * 1.2 * density + 0.4));
          for (var i = 0; i < n; i++) {
            var s = cloudSprite(ch * R(0.8, 1.15, 2), col, shade, dpr);
            clouds.push({
              s: s,
              depth: depth,
              x: R(-s.w, w, 1),
              y: R(-0.12, 0.72, 2) * h - s.h * 0.3 + (1 - depth) * h * 0.04,
              v: (6 + 26 * depth) * R(0.8, 1.2, 2),
              a: 0.5 + 0.5 * depth
            });
          }
        });
        clouds.sort(function (a, b) { return a.depth - b.depth; });
      }

      function resize() {
        var nw = el.clientWidth, nh = el.clientHeight;
        var nd = Math.min(2, window.devicePixelRatio || 1);
        if (nw === w && nh === h && nd === dpr && clouds.length) return;
        w = nw; h = nh; dpr = nd;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        makeClouds();
        draw();
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        for (var i = 0; i < clouds.length; i++) {
          var c = clouds[i];
          ctx.globalAlpha = c.a;
          ctx.drawImage(c.s.img, c.x, c.y, c.s.w, c.s.h);
        }
        ctx.globalAlpha = 1;
      }

      function advance(dt) {
        for (var i = 0; i < clouds.length; i++) {
          var c = clouds[i];
          c.x += (dir * c.v * speed * dt) / 1000;
          // Off one edge: come back in from the other, at a new height.
          if (dir > 0 && c.x > w) { c.x = -c.s.w - R(0, w * 0.3, 1); c.y = R(-0.12, 0.72, 2) * h - c.s.h * 0.3; }
          if (dir < 0 && c.x < -c.s.w) { c.x = w + R(0, w * 0.3, 1); c.y = R(-0.12, 0.72, 2) * h - c.s.h * 0.3; }
        }
      }

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

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-drifting-clouds style="background: linear-gradient(#3b82f6, #bfdbfe); color: #fff">
  <h1>Blue skies ahead</h1>
</section>