← Particles

Glitter Pour

A fine stream of glitter pours from the top of a section, fanning out as it falls and twinkling as each fleck turns in the light. It settles in a shimmering drift along the bottom, and the pour follows your pointer or finger.

Pour some sparkle

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-glitter-pour to a section with a dark background. Move the pointer, or drag a finger, across it to steer the pour.

Options

Add any of these attributes to the same element.

data-rateFlecks per second. Default 260.
data-sizeFleck size in px. Default 2.
data-colorsComma-separated glitter colours. Default golds, pinks and lilacs.
data-follow="false"Don’t follow the pointer: the pour just wanders.

It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion you see a still stream and drift.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Glitter Pour: a fine stream of glitter pours from the top of a section,
    // fanning out as it falls and twinkling as the flecks turn in the light.
    // It settles along the bottom in a shimmering drift, which slowly fades.
    // The pour wanders by itself and follows the pointer (or your finger) when
    // it's over the section. Drawn on a transparent canvas behind your content,
    // and paused when off screen.
    // Usage: <section data-glitter-pour>…</section>
    // Optional: data-rate (flecks per second, default 260), data-size (fleck
    // size in px, default 2), data-colors="#facc15,#f9a8d4,…" (glitter colours),
    // data-follow="false" (don't follow the pointer).
    var DEFAULTS = {
      rate: 260, size: 2,
      colors: ['#fde047', '#facc15', '#fef3c7', '#f9a8d4', '#e9d5ff', '#c4b5fd', '#ffffff', '#fbbf24']
    };

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

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'glp-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var rate = d.rate !== undefined ? +d.rate : DEFAULTS.rate;
      var size = +d.size || DEFAULTS.size;
      var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : DEFAULTS.colors;
      var follow = d.follow !== 'false';
      var R = anime.utils.random;
      var w = 0, h = 0, dpr = 1, t = 0, carry = 0;
      var falling = [], landed = [];
      var src = 0.5, pointerX = null, pointerUntil = 0;
      var MAX_LANDED = 900;

      function fleck(x, y, vx, vy) {
        return {
          x: x, y: y, vx: vx, vy: vy,
          s: size * R(0.6, 1.4, 2),
          c: colors[Math.floor(Math.random() * colors.length)],
          tw: R(2, 7, 2),                 // twinkle speed
          ph: Math.random() * Math.PI * 2,
          flut: R(0.6, 1.6, 2),           // how much it flutters sideways
          life: 0
        };
      }

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
      }

      // How bright a fleck is right now: mostly dim, now and then catching the light.
      function shine(p) {
        var s = Math.sin(t * 0.001 * p.tw + p.ph);
        return s > 0.93 ? (s - 0.93) / 0.07 : 0;
      }

      function drawFleck(p, alpha) {
        var b = shine(p);
        ctx.globalAlpha = alpha * (0.55 + b * 0.45);
        ctx.fillStyle = p.c;
        // A tiny tilted square that narrows as it turns.
        var turn = Math.abs(Math.cos(t * 0.002 * p.tw + p.ph));
        ctx.fillRect(p.x - p.s / 2, p.y - (p.s * (0.35 + turn * 0.65)) / 2, p.s, p.s * (0.35 + turn * 0.65));
        if (b > 0.3) {
          // A little four-pointed glint.
          var g = p.s * (1 + b * 2.4);
          ctx.globalAlpha = alpha * b;
          ctx.fillStyle = '#fff';
          ctx.fillRect(p.x - g, p.y - 0.5, g * 2, 1);
          ctx.fillRect(p.x - 0.5, p.y - g, 1, g * 2);
        }
      }

      function sourceX() {
        return src * w;
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        ctx.globalCompositeOperation = 'lighter';
        for (var i = 0; i < landed.length; i++) {
          var l = landed[i];
          drawFleck(l, Math.min(1, (l.ttl - l.life) / 1500));
        }
        for (var j = 0; j < falling.length; j++) drawFleck(falling[j], 1);
        ctx.globalCompositeOperation = 'source-over';
        ctx.globalAlpha = 1;
      }

      function advance(dt) {
        t += dt;
        // Where the stream comes from: the pointer if it's over us, else a slow wander.
        var wander = 0.5 + Math.sin(t * 0.00033) * 0.28 + Math.sin(t * 0.00091 + 1) * 0.08;
        var goal = pointerX !== null && t < pointerUntil ? pointerX / w : wander;
        src += (goal - src) * Math.min(1, dt * 0.004);

        carry += (rate * dt) / 1000;
        var sx = sourceX();
        while (carry >= 1) {
          carry -= 1;
          falling.push(fleck(sx + R(-3, 3, 2), -2, R(-0.035, 0.035, 3), R(0.02, 0.09, 3)));
        }

        var s = dt / 16;
        for (var i = falling.length - 1; i >= 0; i--) {
          var p = falling[i];
          p.life += dt;
          p.vy = Math.min(p.vy + 0.012 * s, 0.75);            // gravity, with a terminal speed
          p.vx += Math.sin(t * 0.004 * p.flut + p.ph) * 0.0016 * s * p.flut; // flutter
          p.vx *= 0.995;
          p.x += p.vx * dt;
          p.y += p.vy * s * 4;
          // Settle along the bottom with a tiny skid.
          var floor = h - 2 - p.s / 2 - (p.ph % 1) * 5;
          if (p.y >= floor) {
            p.y = floor;
            p.x += p.vx * 40;
            p.life = 0;
            p.ttl = R(3500, 7000);
            falling.splice(i, 1);
            landed.push(p);
            continue;
          }
          if (p.x < -10 || p.x > w + 10) falling.splice(i, 1);
        }
        for (var k = landed.length - 1; k >= 0; k--) {
          landed[k].life += dt;
          if (landed[k].life > landed[k].ttl) landed.splice(k, 1);
        }
        if (landed.length > MAX_LANDED) landed.splice(0, landed.length - MAX_LANDED);
      }

      resize();
      if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);

      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        // A still frame: a frozen stream and a little drift at the bottom.
        for (var f = 0; f < 260; f++) {
          var y = Math.random() * h;
          var spread = (y / h) * 26;
          falling.push(fleck(w * 0.5 + R(-spread, spread), y, 0, 0));
        }
        for (var g = 0; g < 200; g++) {
          var lx = w * 0.5 + (Math.random() - 0.5) * Math.random() * w * 0.5;
          var lf = fleck(lx, h - 2 - Math.random() * 5, 0, 0);
          lf.life = 0; lf.ttl = 1e9;
          landed.push(lf);
        }
        t = 1234;
        draw();
        return;
      }

      if (follow) {
        el.addEventListener('pointermove', function (e) {
          pointerX = e.clientX - el.getBoundingClientRect().left;
          pointerUntil = t + (e.pointerType === 'mouse' ? 400 : 1600);
        });
        el.addEventListener('pointerdown', function (e) {
          pointerX = e.clientX - el.getBoundingClientRect().left;
          pointerUntil = t + 1600;
        });
      }

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

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-glitter-pour style="background: #1e1033; color: #fff">
  <h1>Pour some sparkle</h1>
</section>