A dark feature section with six cells in one bordered panel. The lines between the cells carry light: a short coloured beam travels along each horizontal and vertical divider at its own speed, with a soft bloom around it, and when two beams reach the same crossing a small flare appears there. The beams are drawn on a canvas laid over the grid, so the cells stay ordinary HTML. The grid is three columns on desktop, two on tablets and one on phones, and the beams follow whichever layout is showing.
// Adapted from Cult UI (https://cult-ui.com/docs/components/grid-beam)
// Copyright (c) 2023 Jordan-Gilliam. MIT License: https://we.inc/components/licenses
import { useEffect, useRef, useState } from "react";
import { Boxes, GitBranch, Globe, KeyRound, Radio, Timer } from "lucide-react";
const features = [
{ icon: Radio, title: "Live event stream", text: "Every order, scan and handover arrives on one stream within a second of happening." },
{ icon: GitBranch, title: "Rules that branch", text: "Route by weight, postcode or promise date. Change a rule and it applies to the next parcel." },
{ icon: Globe, title: "Forty carrier links", text: "Label, track and cancel through one API, whichever carrier ends up moving the box." },
{ icon: Timer, title: "Honest delivery dates", text: "Estimates are rebuilt from live depot delays, not from last year's averages." },
{ icon: Boxes, title: "Batch without waiting", text: "Send ten thousand labels in one call and collect them as each one is ready." },
{ icon: KeyRound, title: "Keys per warehouse", text: "Give each site its own scoped key and see exactly which one made every request." },
];
type RGB = [number, number, number];
type Band = { color: RGB; op: number };
/** Beam colours. `h` cycles over the horizontal dividers, `v` over the vertical ones. */
const PALETTE: { h: Band[]; v: Band[] } = {
h: [
{ color: [255, 50, 100], op: 0.9 },
{ color: [40, 180, 220], op: 0.8 },
{ color: [50, 200, 80], op: 0.9 },
{ color: [180, 40, 240], op: 0.8 },
{ color: [255, 160, 30], op: 0.9 },
],
v: [
{ color: [40, 140, 255], op: 0.9 },
{ color: [240, 50, 180], op: 0.8 },
{ color: [30, 185, 170], op: 0.9 },
],
};
/** Seconds for a beam to cross the grid. */
const DURATION = 3.4;
const gaussian = (x: number, s: number) => Math.exp(-(x * x) / (2 * s * s));
const rgba = (c: RGB, a: number, lift = 0) => `rgba(${Math.min(255, c[0] + lift)},${Math.min(255, c[1] + lift)},${Math.min(255, c[2] + lift)},${Math.max(0, a).toFixed(4)})`;
export default function FeatureGridWithLightBeamsOnDividerLines() {
const grid = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null);
const [cols, setCols] = useState(3);
const rows = Math.ceil(features.length / cols);
// Match the canvas grid to the CSS grid: 1 column on phones, 2 on tablets, 3 on desktop.
useEffect(() => {
const el = grid.current;
if (!el) return;
const ro = new ResizeObserver(() => setCols(el.offsetWidth < 560 ? 1 : el.offsetWidth < 860 ? 2 : 3));
ro.observe(el);
return () => ro.disconnect();
}, []);
useEffect(() => {
const cv = canvas.current;
const ctx = cv?.getContext("2d");
if (!cv || !ctx) return;
const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
let w = 0;
let h = 0;
let frame = 0;
let visible = true;
const t0 = performance.now();
const resize = () => {
w = cv.offsetWidth;
h = cv.offsetHeight;
cv.width = w * dpr;
cv.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (still) draw(t0 + 1700);
};
/** One beam: a soft bloom plus a bright core that fades at both ends. */
const beam = (x: number, y: number, len: number, horizontal: boolean, band: Band, gain: number) => {
const bloom = ctx.createRadialGradient(x, y, 0, x, y, len * 1.1);
bloom.addColorStop(0, rgba(band.color, band.op * 0.5 * gain));
bloom.addColorStop(0.4, rgba(band.color, band.op * 0.2 * gain));
bloom.addColorStop(1, "transparent");
ctx.save();
ctx.translate(x, y);
ctx.scale(horizontal ? 1 : 10 / (len * 1.1), horizontal ? 10 / (len * 1.1) : 1);
ctx.translate(-x, -y);
ctx.fillStyle = bloom;
ctx.beginPath();
ctx.arc(x, y, len * 1.1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
const core = horizontal ? ctx.createLinearGradient(x - len, y, x + len, y) : ctx.createLinearGradient(x, y - len, x, y + len);
core.addColorStop(0, "transparent");
core.addColorStop(0.12, rgba(band.color, band.op * 0.4 * gain));
core.addColorStop(0.35, rgba(band.color, band.op * 0.8 * gain, 60));
core.addColorStop(0.5, rgba(band.color, band.op * gain, 100));
core.addColorStop(0.65, rgba(band.color, band.op * 0.8 * gain, 60));
core.addColorStop(0.88, rgba(band.color, band.op * 0.4 * gain));
core.addColorStop(1, "transparent");
ctx.strokeStyle = core;
ctx.lineWidth = 2;
ctx.beginPath();
if (horizontal) {
ctx.moveTo(x - len, y);
ctx.lineTo(x + len, y);
} else {
ctx.moveTo(x, y - len);
ctx.lineTo(x, y + len);
}
ctx.stroke();
};
const draw = (now: number) => {
const elapsed = (now - t0) / 1000;
const cellW = w / cols;
const cellH = h / rows;
const fade = Math.min(1, elapsed / 0.8);
const gain = fade * fade * (3 - 2 * fade);
const breathe = 0.85 + 0.3 * Math.sin(elapsed * 1.4) + 0.1 * Math.sin(elapsed * 2.3);
const hx = (r: number) => (((elapsed * (1 + (r % 3) * 0.12)) / DURATION + r * 0.21 + (r % 2) * 0.35) % 1) * w;
const vy = (c: number) => (((elapsed * (1 + (c % 3) * 0.1)) / (DURATION * 1.2) + c * 0.26 + (c % 2) * 0.4) % 1) * h;
ctx.clearRect(0, 0, w, h);
for (let r = 1; r < rows; r++) beam(hx(r), r * cellH, Math.min(cellW, 360) * 0.6 * breathe, true, PALETTE.h[r % PALETTE.h.length], gain);
for (let c = 1; c < cols; c++) beam(c * cellW, vy(c), Math.min(cellH, 360) * 0.6 * breathe, false, PALETTE.v[c % PALETTE.v.length], gain);
// A flare where a horizontal and a vertical beam pass through the same crossing.
for (let r = 1; r < rows; r++) {
for (let c = 1; c < cols; c++) {
const prox = gaussian((hx(r) - c * cellW) / cellW, 0.25) * gaussian((vy(c) - r * cellH) / cellH, 0.25);
if (prox <= 0.05) continue;
const a = PALETTE.h[r % PALETTE.h.length].color;
const b = PALETTE.v[c % PALETTE.v.length].color;
const mix: RGB = [(a[0] + b[0]) >> 1, (a[1] + b[1]) >> 1, (a[2] + b[2]) >> 1];
const radius = 14 * Math.sqrt(prox);
const flare = ctx.createRadialGradient(c * cellW, r * cellH, 0, c * cellW, r * cellH, radius);
flare.addColorStop(0, rgba(mix, prox * 0.8 * gain, 140));
flare.addColorStop(0.5, rgba(mix, prox * 0.3 * gain));
flare.addColorStop(1, "transparent");
ctx.fillStyle = flare;
ctx.beginPath();
ctx.arc(c * cellW, r * cellH, radius, 0, Math.PI * 2);
ctx.fill();
}
}
};
const loop = (now: number) => {
draw(now);
frame = requestAnimationFrame(loop);
};
const start = () => {
cancelAnimationFrame(frame);
if (!still && visible && !document.hidden) frame = requestAnimationFrame(loop);
};
const ro = new ResizeObserver(resize);
const io = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting;
start();
});
ro.observe(cv);
io.observe(cv);
document.addEventListener("visibilitychange", start);
resize();
start();
return () => {
cancelAnimationFrame(frame);
ro.disconnect();
io.disconnect();
document.removeEventListener("visibilitychange", start);
};
}, [cols, rows]);
return (
<section className="bg-neutral-950 px-4 py-16 sm:px-6 sm:py-24">
<div className="mx-auto max-w-6xl">
<div className="max-w-2xl">
<p className="text-sm font-semibold uppercase tracking-widest text-cyan-300">Parcel infrastructure</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-white sm:text-5xl">One API between your checkout and the doorstep.</h2>
<p className="mt-4 text-base leading-7 text-neutral-400">Six pieces that work on their own and fit together when you need all of them.</p>
</div>
<div ref={grid} className="relative mt-12 overflow-hidden rounded-3xl border border-white/10 bg-neutral-900/60">
<ul className="grid auto-rows-fr" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>
{features.map((f, i) => (
<li key={f.title} className={`p-6 sm:p-8 ${i % cols ? "border-l border-white/10" : ""} ${i >= cols ? "border-t border-white/10" : ""}`}>
<span className="flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-white">
<f.icon aria-hidden="true" className="h-5 w-5" />
</span>
<h3 className="mt-5 text-lg font-semibold text-white">{f.title}</h3>
<p className="mt-2 text-sm leading-6 text-neutral-400">{f.text}</p>
</li>
))}
</ul>
{/* The beams are painted on top of the hairline borders between cells. */}
<canvas ref={canvas} aria-hidden="true" className="pointer-events-none absolute inset-0 h-full w-full" />
</div>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/feature-grid-with-light-beams-on-divider-lines.json
A canvas covers the grid. Each frame, for every divider, it works out how far along the beam is, then paints a wide faint radial gradient squashed into a strip for the bloom and a 2 pixel line with a gradient that is brightest in the middle for the core.
For each crossing the code measures how close the horizontal beam and the vertical beam are to that point. The two closeness values are multiplied, so the flare only shows when both are near at once, and it takes the average of their colours.
The grid uses `auto-rows-fr`, so every row is the same height and dividers sit at even fractions. The component watches the grid width, sets the column count in state, and both the CSS grid and the canvas use that one number.
It draws a handful of gradients per frame, one per divider. The loop stops when the section leaves the screen or the tab is hidden, and the pixel ratio is capped at 2.
Yes. Switch the panel and text classes to light ones and lower the colour values in `PALETTE` (darker, more saturated colours at an `op` near 0.3 read better on white).
Adapted from Grid Beam in Cult UI, used under its open-source licence. Licences and credits
Related: Bento Grid with Six Feature Tiles, Animated Grid Background with Travelling Light Beams, Three Column Feature Grid with Icons.
More feature sections or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.