A light grid background for a product hero. Thin lines form a 48 pixel grid that fades out toward the edges, and nine beams of orange light race along individual grid lines, some across and some down, each with a bright head and a fading tail. A handful of cells glow softly in and out between the beams. It is built with CSS gradients and keyframes only, and the hero on top has a badge, headline, two buttons and a stats strip that stacks on phones.
import { useEffect, useRef, useState } from "react";
const CELL = 48; // grid size in pixels; beams and lit cells snap to it
// line: which grid line the beam rides (counted from the top or the left edge).
const beams = [
{ dir: "x", line: 1, seconds: 4.6, delay: -3.2, reverse: true },
{ dir: "x", line: 3, seconds: 5.5, delay: 0, reverse: false },
{ dir: "x", line: 7, seconds: 7, delay: -2.6, reverse: true },
{ dir: "x", line: 9, seconds: 5, delay: -1.5, reverse: false },
{ dir: "x", line: 12, seconds: 6.2, delay: -4.4, reverse: false },
{ dir: "x", line: 13, seconds: 5.2, delay: -0.9, reverse: true },
{ dir: "y", line: 2, seconds: 5, delay: -1.2, reverse: false },
{ dir: "y", line: 6, seconds: 6.5, delay: -3.8, reverse: true },
{ dir: "y", line: 11, seconds: 5.8, delay: -0.4, reverse: false },
{ dir: "y", line: 17, seconds: 7.2, delay: -5.1, reverse: true },
{ dir: "y", line: 22, seconds: 5.4, delay: -2.2, reverse: false },
{ dir: "y", line: 26, seconds: 6.8, delay: -4.6, reverse: true },
];
// Cells that glow softly in and out: [column, row, delay in seconds].
const cells = [
[1, 2, 0], [4, 8, 1.4], [6, 4, 2.6], [10, 9, 0.8], [15, 2, 3.1], [19, 10, 1.9], [21, 5, 0.3], [24, 8, 2.2], [3, 11, 3.4], [25, 1, 1.1],
];
const stats = [
{ value: "41 ms", label: "median delivery" },
{ value: "9.2 bn", label: "events a month" },
{ value: "99.995%", label: "uptime, last 12 months" },
];
export default function AnimatedGridBackgroundWithTravellingLightBeams() {
const ref = useRef<HTMLElement>(null);
const [paused, setPaused] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el || typeof IntersectionObserver === "undefined") return;
const io = new IntersectionObserver(([entry]) => setPaused(!entry.isIntersecting));
io.observe(el);
return () => io.disconnect();
}, []);
const fade = "radial-gradient(ellipse 85% 75% at 50% 45%, black 35%, transparent 100%)";
return (
<section ref={ref} className={`grid-beams relative isolate overflow-hidden bg-[#faf8f5] text-stone-900 ${paused ? "grid-beams-paused" : ""}`}>
<style>{`
@keyframes grid-beams-x { from { transform: translate3d(-50%, 0, 0); } to { transform: translate3d(50%, 0, 0); } }
@keyframes grid-beams-y { from { transform: translate3d(0, -50%, 0); } to { transform: translate3d(0, 50%, 0); } }
@keyframes grid-beams-cell { 0%, 100% { opacity: 0; } 45%, 60% { opacity: 1; } }
@keyframes grid-beams-rise { from { opacity: 0; transform: translate3d(0, 16px, 0); } to { opacity: 1; transform: none; } }
.grid-beams-x { background: linear-gradient(90deg, transparent calc(50% - 340px), rgba(251,146,60,0.45) calc(50% - 160px), #f97316 calc(50% - 14px), #e11d48 50%, transparent 50%); filter: drop-shadow(0 0 5px rgba(249,115,22,1)) drop-shadow(0 0 14px rgba(244,63,94,0.55)); animation: grid-beams-x linear infinite; }
.grid-beams-y { background: linear-gradient(180deg, transparent calc(50% - 340px), rgba(251,146,60,0.45) calc(50% - 160px), #f97316 calc(50% - 14px), #e11d48 50%, transparent 50%); filter: drop-shadow(0 0 5px rgba(249,115,22,1)) drop-shadow(0 0 14px rgba(244,63,94,0.55)); animation: grid-beams-y linear infinite; }
.grid-beams-cell { animation: grid-beams-cell 6s ease-in-out infinite both; }
.grid-beams-rise { animation: grid-beams-rise 0.8s cubic-bezier(0.16, 1, 0.3, 1) both; }
.grid-beams-paused * { animation-play-state: paused !important; }
@media (prefers-reduced-motion: reduce) { .grid-beams * { animation: none !important; } .grid-beams-cell { opacity: 0.6; } }
`}</style>
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10" style={{ maskImage: fade, WebkitMaskImage: fade }}>
{/* The grid itself: two 1px line patterns */}
<div
className="absolute inset-0"
style={{
backgroundImage: "linear-gradient(to right, rgba(28,25,23,0.11) 1px, transparent 1px), linear-gradient(to bottom, rgba(28,25,23,0.11) 1px, transparent 1px)",
backgroundSize: `${CELL}px ${CELL}px`,
}}
/>
{cells.map(([col, row, delay]) => (
<div
key={`${col}-${row}`}
className="grid-beams-cell absolute bg-gradient-to-br from-orange-400/35 to-rose-400/15"
style={{ left: col * CELL + 1, top: row * CELL + 1, width: CELL - 1, height: CELL - 1, animationDelay: `${delay}s` }}
/>
))}
{beams.map((b, i) =>
b.dir === "x" ? (
<div key={i} className="absolute inset-x-0 h-[2px]" style={{ top: b.line * CELL - 0.5 }}>
<div className="grid-beams-x h-full w-full" style={{ animationDuration: `${b.seconds}s`, animationDelay: `${b.delay}s`, scale: b.reverse ? "-1 1" : undefined }} />
</div>
) : (
<div key={i} className="absolute inset-y-0 w-[2px]" style={{ left: b.line * CELL - 0.5 }}>
<div className="grid-beams-y h-full w-full" style={{ animationDuration: `${b.seconds}s`, animationDelay: `${b.delay}s`, scale: b.reverse ? "1 -1" : undefined }} />
</div>
),
)}
</div>
{/* Soft wash behind the copy so the lines never fight the text */}
<div aria-hidden="true" className="pointer-events-none absolute left-1/2 top-[42%] -z-10 h-[380px] w-[min(700px,92%)] -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-[#faf8f5] opacity-80 blur-3xl" />
<div className="mx-auto flex min-h-[680px] max-w-5xl flex-col items-center justify-center px-6 py-20 text-center">
<p className="grid-beams-rise inline-flex items-center gap-2 rounded-full border border-stone-300 bg-white px-3 py-1.5 text-xs font-medium text-stone-700 shadow-sm" style={{ animationDelay: "0.05s" }}>
<span className="rounded-full bg-orange-600 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-white">New</span>
Replay any event from the last 30 days
</p>
<h1 className="grid-beams-rise mt-7 max-w-3xl text-4xl font-semibold leading-[1.05] tracking-tight text-stone-950 sm:text-6xl" style={{ animationDelay: "0.15s" }}>
Every event, delivered
<br className="hidden sm:block" /> <span className="text-orange-600">exactly once</span>
</h1>
<p className="grid-beams-rise mt-6 max-w-xl text-base leading-relaxed text-stone-600 sm:text-lg" style={{ animationDelay: "0.25s" }}>
Relaywire sits between your services and routes webhooks, queues and streams with retries, ordering and a full audit trail built in.
</p>
<div className="grid-beams-rise mt-9 flex w-full flex-col items-center justify-center gap-3 sm:w-auto sm:flex-row" style={{ animationDelay: "0.35s" }}>
<a href="#" className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-stone-950 px-6 text-sm font-semibold text-white shadow-lg shadow-stone-900/20 transition hover:bg-stone-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-600 focus-visible:ring-offset-2 sm:w-auto">
Start routing free
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-4 w-4">
<path d="M4 10h12M11 5l5 5-5 5" />
</svg>
</a>
<a href="#" className="inline-flex h-12 w-full items-center justify-center rounded-lg border border-stone-300 bg-white px-6 text-sm font-semibold text-stone-900 transition hover:border-stone-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-600 focus-visible:ring-offset-2 sm:w-auto">
Read the docs
</a>
</div>
<dl className="grid-beams-rise mt-14 grid w-full max-w-3xl grid-cols-1 gap-px overflow-hidden rounded-xl border border-stone-200 bg-stone-200 shadow-sm sm:grid-cols-3" style={{ animationDelay: "0.5s" }}>
{stats.map((s) => (
<div key={s.label} className="bg-white px-6 py-5 text-left">
<dd className="text-2xl font-semibold tracking-tight tabular-nums text-stone-950">{s.value}</dd>
<dt className="mt-1 text-sm text-stone-600">{s.label}</dt>
</div>
))}
</dl>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/animated-grid-background-with-travelling-light-beams.json
Each beam lives in a 2 pixel tall (or wide) strip that is pinned exactly on a grid line using `line * CELL`. Inside the strip is a full-size element whose background is a gradient: transparent, then a short ramp of orange that ends in a hard edge at the midpoint. Sliding that element from minus half to plus half of its own size with `translate3d` carries the bright edge from one side of the section to the other.
Animating `left` or `top` makes the browser redo layout on every frame. Moving the element with `transform` is handled by the compositor and stays smooth even with many beams. This trick also needs no knowledge of the container width, so it works at any screen size.
Two `linear-gradient` backgrounds, one for vertical lines and one for horizontal, each one pixel of colour followed by transparent, repeated with `background-size` set to the cell size. A radial `mask-image` fades the whole layer toward the edges.
Yes. Raise `seconds` for a calmer feel, and delete entries from `beams` to reduce the count. Three or four beams with long durations work well behind dense content.
No. Only React is imported, and only to pause the animation while the section is off screen. The effect itself is CSS.
Related: Dot Matrix Background with Ripples and Pointer Spotlight, Particle Network Background with Connecting Lines, Aurora Background with Drifting Colour Bands.
More animated backgrounds 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.