One soft beam follows the pointer across a grid, lighting the borders and faces of the cards it passes.
Fast
Secure
Global
Typed
Customize
Usage
import { SpotlightGrid, SpotlightGridItem } from "@/components/motionkit/spotlight-grid";
<SpotlightGrid className="grid gap-4 sm:grid-cols-3">
{features.map((feature) => (
<SpotlightGridItem key={feature.title}>
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</SpotlightGridItem>
))}
</SpotlightGrid>Motion
The exact curves this component runs, plotted with Motion's own maths.
Light fade
Pointer enters the grid
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/spotlight-grid.tsx.
"use client";
import { useRef, type CSSProperties, type PointerEvent, type ReactNode } from "react";
type SpotlightGridProps = {
children?: ReactNode;
/** Colour of the beam. */
color?: string;
/** Radius of the beam in pixels. */
radius?: number;
className?: string;
};
type SpotlightGridItemProps = {
children?: ReactNode;
className?: string;
};
/**
* A grid lit by one beam that follows the pointer. Every <SpotlightGridItem> inside
* lights up where the beam falls on it, so neighbouring cards glow at their nearest edges.
*/
export function SpotlightGrid({ children, color = "#5FAEB4", radius = 320, className = "" }: SpotlightGridProps) {
const ref = useRef<HTMLDivElement>(null);
// positions are written straight to CSS variables, so moving the pointer never re-renders React
function handleMove(event: PointerEvent<HTMLDivElement>) {
const grid = ref.current;
if (!grid) return;
const items = grid.querySelectorAll<HTMLElement>("[data-spotlight-item]");
for (const item of items) {
const rect = item.getBoundingClientRect();
item.style.setProperty("--spot-x", `${event.clientX - rect.left}px`);
item.style.setProperty("--spot-y", `${event.clientY - rect.top}px`);
}
grid.style.setProperty("--spot-on", "1");
}
return (
<div
ref={ref}
onPointerMove={handleMove}
onPointerLeave={() => ref.current?.style.setProperty("--spot-on", "0")}
style={{ "--spot-color": color, "--spot-radius": `${radius}px`, "--spot-on": 0 } as CSSProperties}
className={className}
>
{children}
</div>
);
}
export function SpotlightGridItem({ children, className = "" }: SpotlightGridItemProps) {
return (
<div
data-spotlight-item
className={`group relative rounded-2xl bg-white/[0.06] p-px focus-within:[--spot-focus:1] ${className}`}
>
{/* the border: the beam shows through the 1px gap around the card's face */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 rounded-[inherit] transition-opacity duration-300"
style={{
opacity: "max(var(--spot-on), var(--spot-focus, 0))",
background:
"radial-gradient(var(--spot-radius) circle at var(--spot-x, 50%) var(--spot-y, 50%), var(--spot-color), transparent 70%)",
}}
/>
<div className="relative h-full rounded-[15px] bg-[#0C1010]">
{/* a fainter wash of the same beam across the face */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 rounded-[inherit] transition-opacity duration-300"
style={{
opacity: "calc(max(var(--spot-on), var(--spot-focus, 0)) * 0.14)",
background:
"radial-gradient(calc(var(--spot-radius) * 0.8) circle at var(--spot-x, 50%) var(--spot-y, 50%), var(--spot-color), transparent 70%)",
}}
/>
<div className="relative h-full p-6">{children}</div>
</div>
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| color | string | "#5FAEB4" | Colour of the beam. |
| radius | number | 320 | Radius of the beam in pixels. |
| <SpotlightGridItem> | component | — | Wrap each card in one. Lay them out with your own grid classes. |
Realtime sync
Move your cursor across this card.
Spotlight Card
A card with a soft glow that tracks the pointer across its surface.
Edge functions
Tilt me with your cursor.
Tilt Card
A 3D card that tilts toward the pointer, with an optional moving glare.
Launch kit
Tilt me to see the layers.
Depth Card
A card that tilts toward the cursor while its layers float at different depths, so each moves at its own speed.