Grid Beams
A faint grid sits behind a section and thin beams of light shoot along its lines, each a bright head with a fading tail that lights up the crossings it passes. The edges fade away so the grid frames your content.
Ship faster
How to use it
- Paste the snippet just before
</head>. - Add
data-grid-beamsto a section with a dark background.
Options
Add any of these attributes to the same element.
data-colors | Beam colours, comma-separated. Default violet, cyan and pink. |
data-grid | Grid line colour. Default rgba(255,255,255,.08). |
data-size | Grid cell size in px. Default 48. |
data-rate | Beams per second for a 600 × 360 section, scaled up or down with its area. Default 1.6. |
data-speed | Speed multiplier. Default 1. |
data-fade="false" | No fade at the edges. |
It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion a few beams hold still on the grid.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Grid Beams: a faint grid sits behind a section and thin beams of light
// shoot along its lines, each one a bright head with a fading tail that
// lights up the crossings it passes. The edges fade away so it frames your
// content. Drawn on a transparent canvas, and paused when off screen.
// Usage: <section data-grid-beams style="background:#0a0a14">…</section>
// Optional: data-colors (beam colours, comma-separated, default violet,
// cyan and pink), data-grid (grid line colour, default
// rgba(255,255,255,.08)), data-size (cell size in px, default 48),
// data-rate (beams per second for a 600 by 360 section, scaled by area,
// default 1.6), data-speed (multiplier, default 1), data-fade="false"
// (no fade at the edges).
var DEFAULTS = { colors: '#a855f7,#22d3ee,#f472b6', grid: 'rgba(255,255,255,.08)', size: 48, rate: 1.6, speed: 1 };
var css =
'[data-grid-beams]{position:relative;isolation:isolate;overflow:hidden}' +
'.gbm-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}' +
'.gbm-fade{-webkit-mask-image:radial-gradient(ellipse 72% 72% at 50% 50%,#000 40%,transparent 100%);mask-image:radial-gradient(ellipse 72% 72% at 50% 50%,#000 40%,transparent 100%)}';
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.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 build(el) {
var d = el.dataset;
var cols = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return rgb(c).join(','); });
var gridCol = d.grid || DEFAULTS.grid;
var size = +d.size || DEFAULTS.size;
var rate = d.rate !== undefined ? +d.rate : DEFAULTS.rate;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var canvas = document.createElement('canvas');
canvas.className = 'gbm-canvas' + (d.fade === 'false' ? '' : ' gbm-fade');
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, ox = 0, oy = 0, nx = 0, ny = 0, beams = [], due = 0;
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;
// Centre the grid so a line runs through the middle both ways.
ox = ((w / 2) % size);
oy = ((h / 2) % size);
nx = Math.ceil((w - ox) / size) + 1;
ny = Math.ceil((h - oy) / size) + 1;
beams = [];
if (still) for (var i = 0; i < 6; i++) { spawn(); beams[i].p = R(0.3, 0.8, 2) * beams[i].span; }
draw();
}
function spawn() {
var across = Math.random() < w / (w + h); // horizontal beams on wide sections
var line = Math.round(across ? oy + R(0, ny - 1) * size : ox + R(0, nx - 1) * size);
var span = across ? w : h;
beams.push({
across: across,
line: line,
dir: Math.random() < 0.5 ? 1 : -1,
span: span,
p: -size * 3,
len: size * R(20, 40) / 10,
v: R(160, 300) * speed,
col: cols[R(0, cols.length - 1)]
});
}
// Where along its line the beam's head is, as x and y.
function at(b, p) {
var s = b.dir > 0 ? p : b.span - p;
return b.across ? [s, b.line] : [b.line, s];
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = gridCol;
ctx.lineWidth = 1;
ctx.beginPath();
for (var i = 0; i < nx; i++) { var x = Math.round(ox + i * size) + 0.5; ctx.moveTo(x, 0); ctx.lineTo(x, h); }
for (var j = 0; j < ny; j++) { var y = Math.round(oy + j * size) + 0.5; ctx.moveTo(0, y); ctx.lineTo(w, y); }
ctx.stroke();
ctx.globalCompositeOperation = 'lighter';
ctx.lineCap = 'round';
for (var k = 0; k < beams.length; k++) {
var b = beams[k];
var head = at(b, b.p), tail = at(b, b.p - b.len);
var g = ctx.createLinearGradient(tail[0], tail[1], head[0], head[1]);
g.addColorStop(0, 'rgba(' + b.col + ',0)');
g.addColorStop(1, 'rgba(' + b.col + ',1)');
ctx.strokeStyle = g;
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.moveTo(tail[0] + 0.5, tail[1] + 0.5);
ctx.lineTo(head[0] + 0.5, head[1] + 0.5);
ctx.stroke();
// A soft glow round the head, and the nearest crossing lights up.
var glow = ctx.createRadialGradient(head[0], head[1], 0, head[0], head[1], 14);
glow.addColorStop(0, 'rgba(' + b.col + ',0.7)');
glow.addColorStop(1, 'rgba(' + b.col + ',0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(head[0], head[1], 14, 0, TAU);
ctx.fill();
var cross = b.across ? head[0] - ox : head[1] - oy;
var near = Math.abs(cross - Math.round(cross / size) * size);
if (near < size * 0.3) {
var cx = b.across ? ox + Math.round(cross / size) * size : b.line;
var cy = b.across ? b.line : oy + Math.round(cross / size) * size;
ctx.fillStyle = 'rgba(' + b.col + ',' + (0.5 * (1 - near / (size * 0.3))).toFixed(3) + ')';
ctx.beginPath();
ctx.arc(cx + 0.5, cy + 0.5, 2.5, 0, TAU);
ctx.fill();
}
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(head[0] + 0.5, head[1] + 0.5, 1.4, 0, TAU);
ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
}
function advance(dt) {
var s = dt / 1000;
// More beams on bigger sections, so the grid feels equally busy.
due -= s * rate * Math.max(0.4, (w * h) / (600 * 360));
while (due <= 0) { spawn(); due += R(40, 160) / 100; }
for (var i = beams.length - 1; i >= 0; i--) {
var b = beams[i];
b.p += b.v * s;
if (b.p - b.len > b.span) beams.splice(i, 1);
}
}
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-grid-beams]').forEach(function (el) {
if (el._gbm) return;
el._gbm = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-grid-beams style="background: #0a0a14; color: #fff">
<h1>Ship faster</h1>
</section>