Hexagon Grid Glow
A honeycomb of faint hexagons sits behind a section, and every few seconds a wave of light rolls out across it, each cell glowing as the wave passes and fading slowly after. Cells near the pointer glow too; click or tap to send out a wave of your own.
Tap anywhere
How to use it
- Paste the snippet just before
</head>. - Add
data-hexagon-grid-glowto a section with a dark background.
Options
Add any of these attributes to the same element.
data-color | Glow colour. Default cyan #22d3ee. |
data-size | Cell radius in px. Default 24. |
data-interval | Time between waves. Default 2400 ms. |
data-interactive="false" | Ignore the pointer: no hover glow, no waves on click. |
It’s drawn on a transparent canvas behind your content and paused when off screen. On phones, dragging a finger lights the cells under it. With reduced motion the grid shows without waves.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Hexagon Grid Glow: a honeycomb of faint hexagons behind a section, with
// waves of light rolling out across it every few seconds, each cell
// glowing as a wave passes and fading slowly after. Cells near the pointer
// glow too, and a click or tap sends out a wave of your own. Drawn on a
// transparent canvas behind your content, and paused when off screen.
// Usage: <section data-hexagon-grid-glow style="background:#070b18">…</section>
// Optional: data-color (default cyan #22d3ee), data-size (cell radius in
// px, default 24), data-interval (ms between waves, default 2400),
// data-interactive="false" (ignore the pointer).
var DEFAULTS = { color: '#22d3ee', size: 24, interval: 2400 };
var css =
'[data-hexagon-grid-glow]{position:relative;isolation:isolate;overflow:hidden}' +
'.hxg-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;
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 c = rgb(d.color || DEFAULTS.color);
var col = c[0] + ',' + c[1] + ',' + c[2];
var size = +d.size || DEFAULTS.size;
var interval = +d.interval || DEFAULTS.interval;
var interactive = d.interactive !== 'false' && !el.closest('[aria-hidden="true"]');
var canvas = document.createElement('canvas');
canvas.className = 'hxg-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, now = 0, next = 300, r = size;
var cells = [], waves = [], pointer = null, hex = null;
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
// Slightly smaller cells in small boxes, so there are enough of them.
r = size * Math.min(1, Math.max(0.65, Math.min(w, h) / 320));
// Pointy-topped hexagons: rows 1.5 r apart, every other row shifted.
var cw = Math.sqrt(3) * r, ch = 1.5 * r;
cells = [];
for (var row = -1; row * ch < h + r; row++) {
for (var i = -1; i * cw < w + cw; i++) {
cells.push({ x: i * cw + (row & 1 ? cw / 2 : 0), y: row * ch, g: 0 });
}
}
// One hexagon path, reused for every cell; a small gap between cells.
hex = new Path2D();
var ri = r - Math.max(1.5, r * 0.08);
for (var k = 0; k < 6; k++) {
var a = Math.PI / 6 + (k * Math.PI) / 3;
if (k === 0) hex.moveTo(Math.cos(a) * ri, Math.sin(a) * ri);
else hex.lineTo(Math.cos(a) * ri, Math.sin(a) * ri);
}
hex.closePath();
if (still) draw();
}
function wave(x, y) {
waves.push({ x: x, y: y, start: now, v: Math.max(w, h) * 0.55, life: Math.hypot(w, h) * 1.1 });
}
function update(dt) {
now += dt;
next -= dt;
if (next <= 0) {
wave(R(0.1, 0.9, 3) * w, R(0.1, 0.9, 3) * h);
next = interval * R(0.8, 1.2, 2);
}
var band = r * 1.6;
for (var i = 0; i < cells.length; i++) {
var cell = cells[i], target = 0;
for (var k = 0; k < waves.length; k++) {
var wv = waves[k], rad = ((now - wv.start) / 1000) * wv.v;
var dist = Math.hypot(cell.x - wv.x, cell.y - wv.y);
var off = (dist - rad) / band;
// A ring of light that dims as it spreads.
target += Math.exp(-off * off) * Math.max(0, 1 - rad / wv.life);
}
if (pointer) {
var pd = Math.hypot(cell.x - pointer.x, cell.y - pointer.y) / (r * 2.6);
target += Math.exp(-pd * pd) * 0.9;
}
target = Math.min(1, target);
// Light up fast, fade out slowly.
cell.g = target > cell.g ? cell.g + (target - cell.g) * Math.min(1, dt / 60) : cell.g * Math.pow(0.5, dt / 380);
}
for (var j = waves.length - 1; j >= 0; j--) {
if (((now - waves[j].start) / 1000) * waves[j].v > waves[j].life) waves.splice(j, 1);
}
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.lineWidth = 1;
for (var i = 0; i < cells.length; i++) {
var cell = cells[i], gl = cell.g;
ctx.setTransform(dpr, 0, 0, dpr, cell.x * dpr, cell.y * dpr);
if (gl > 0.02) {
ctx.fillStyle = 'rgba(' + col + ',' + (gl * 0.38).toFixed(3) + ')';
ctx.fill(hex);
}
ctx.strokeStyle = 'rgba(' + col + ',' + (0.13 + gl * 0.75).toFixed(3) + ')';
ctx.stroke(hex);
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
resize();
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
if (still) { draw(); return; }
if (interactive) {
// Works with a mouse (hover) and with touch (drag and tap).
el.addEventListener('pointermove', function (e) {
var b = el.getBoundingClientRect();
pointer = { x: e.clientX - b.left, y: e.clientY - b.top };
});
el.addEventListener('pointerleave', function () { pointer = null; });
el.addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') pointer = null; });
el.addEventListener('pointerdown', function (e) {
var b = el.getBoundingClientRect();
wave(e.clientX - b.left, e.clientY - b.top);
});
}
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var t = performance.now();
var dt = last ? Math.min(50, t - last) : 16;
last = t;
update(dt);
draw();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-hexagon-grid-glow]').forEach(function (el) {
if (el._hxg) return;
el._hxg = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-hexagon-grid-glow style="background: #070b18; color: #fff">
<h1>Tap anywhere</h1>
</section>