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
- Paste the snippet just before
</head>. - Add
data-glitter-pourto 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-rate | Flecks per second. Default 260. |
data-size | Fleck size in px. Default 2. |
data-colors | Comma-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.
<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><section data-glitter-pour style="background: #1e1033; color: #fff">
<h1>Pour some sparkle</h1>
</section>