A dark event hero on a grid of outlined squares. The square under your pointer fills with pink at once and then fades out over a second after you move on, so quick movements leave a trail across the page. When nobody is pointing, one lit square walks around the middle of the grid by itself. The grid is masked to fade at the edges. The hero holds a headline, two buttons and three session cards, which stack on phones.
// Adapted from Magic UI (https://magicui.design/docs/components/interactive-grid-pattern)
// Copyright (c) Magic UI. MIT License: https://we.inc/components/licenses
import { useEffect, useState } from "react";
import { ArrowRight } from "lucide-react";
const COLS = 32;
const ROWS = 14;
const CELL = 48;
const sessions = [
{ day: "Thu", title: "Opening night: the long table", seats: "12 seats left" },
{ day: "Fri", title: "Type that moves, a live build", seats: "Waitlist" },
{ day: "Sat", title: "Portfolio reviews, by appointment", seats: "30 slots" },
];
/**
* A grid of svg squares. The square under the pointer lights up at once and
* fades out slowly when the pointer leaves, which leaves a trail. While
* nobody is pointing, `wander` lights a square that walks around on its own.
*/
function InteractiveGrid({ wander = true }: { wander?: boolean }) {
const [hovered, setHovered] = useState<number | null>(null);
const [auto, setAuto] = useState<number | null>(null);
useEffect(() => {
if (!wander || hovered !== null) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
let col = Math.floor(COLS / 2) + 4;
let row = Math.floor(ROWS / 2) - 2;
const walk = () => {
// One step in a random direction, kept inside the middle of the grid.
if (Math.random() < 0.5) col = Math.min(COLS - 8, Math.max(7, col + (Math.random() < 0.5 ? -1 : 1)));
else row = Math.min(ROWS - 3, Math.max(2, row + (Math.random() < 0.5 ? -1 : 1)));
setAuto(row * COLS + col);
};
walk();
const timer = window.setInterval(walk, 160);
return () => {
window.clearInterval(timer);
setAuto(null);
};
}, [wander, hovered]);
const lit = hovered ?? auto;
return (
<svg aria-hidden="true" width={COLS * CELL} height={ROWS * CELL} className="absolute left-1/2 top-1/2 max-w-none -translate-x-1/2 -translate-y-1/2" onPointerLeave={() => setHovered(null)}>
{Array.from({ length: COLS * ROWS }).map((_, i) => (
<rect
key={i}
x={(i % COLS) * CELL}
y={Math.floor(i / COLS) * CELL}
width={CELL}
height={CELL}
onPointerEnter={() => setHovered(i)}
className={`stroke-white/10 transition-all ease-out motion-reduce:transition-none ${i === lit ? "fill-fuchsia-400/60 duration-100" : "fill-transparent duration-1000"}`}
/>
))}
</svg>
);
}
export default function InteractiveGridBackgroundThatLightsUnderPointer() {
return (
<section className="relative overflow-hidden bg-zinc-950 px-5 py-20 text-white sm:px-6 sm:py-28">
<div className="absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,black_30%,transparent_80%)]">
<InteractiveGrid />
</div>
<span aria-hidden="true" className="pointer-events-none absolute left-1/2 top-0 h-64 w-[700px] max-w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-fuchsia-600/25 blur-3xl" />
{/* The wrapper ignores the pointer so the grid underneath still reacts; links switch it back on. */}
<div className="pointer-events-none relative mx-auto max-w-4xl text-center">
<p className="text-sm font-semibold uppercase tracking-widest text-fuchsia-300">Gridlock, a weekend for interface designers</p>
<h1 className="mt-5 text-4xl font-semibold tracking-tight sm:text-6xl sm:leading-[1.05]">Three days of pixels, people and very good coffee.</h1>
<p className="mx-auto mt-6 max-w-xl text-lg leading-8 text-zinc-300">Talks in the morning, workshops after lunch and a long dinner each night. Move your pointer across the background while you decide.</p>
<div className="mt-9 flex flex-wrap items-center justify-center gap-3">
<a href="#" className="group pointer-events-auto inline-flex h-12 items-center gap-2 rounded-full bg-fuchsia-500 px-6 text-sm font-semibold text-white transition hover:bg-fuchsia-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-fuchsia-300 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950">
Get a weekend pass
<ArrowRight aria-hidden="true" className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</a>
<a href="#" className="pointer-events-auto inline-flex h-12 items-center rounded-full border border-white/20 bg-zinc-950/60 px-6 text-sm font-semibold text-white transition hover:border-white/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-fuchsia-300">
See the schedule
</a>
</div>
<ul className="mt-14 grid gap-3 text-left sm:grid-cols-3">
{sessions.map((s) => (
<li key={s.day} className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5">
<p className="text-xs font-semibold uppercase tracking-widest text-fuchsia-300">{s.day}</p>
<p className="mt-2 font-medium text-white">{s.title}</p>
<p className="mt-3 text-sm text-zinc-400">{s.seats}</p>
</li>
))}
</ul>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/interactive-grid-background-that-lights-under-pointer.json
Lighting up uses a 100 millisecond transition and going dark uses a 1000 millisecond one. A square you have left is already unlit in state, but it is still fading on screen, so several squares glow at once along the path you took.
The grid sits behind the content. The content wrapper has `pointer-events-none` so movement passes through to the grid, and the links have `pointer-events-auto` so they still receive clicks.
There is no hover on touch screens, so the wandering square keeps the background moving. Touching and dragging on the grid also lights the squares under your finger.
A few hundred rectangles are fine. Only two change class on each move. If you need thousands of cells, a canvas version is the better choice.
Yes. Lower `CELL` and raise `COLS` and `ROWS` to cover the same area. Keep the total under about a thousand squares.
Adapted from Interactive Grid Pattern in Magic UI, used under its open-source licence. Licences and credits
Related: Dot Matrix Background with Ripples and Pointer Spotlight, Grid Background with Fading Random Squares, Flickering Pixel Grid Canvas Background.
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.