Interactive
HoverSpotlight
When the mouse comes in, the block goes dark and only a soft circle around the pointer stays lit. The light closes in as the dark fades up. Touch and keyboard see the block fully lit.
Download
- React
- Tailwind CSS v4
- motion
components/ui/hover-spotlight.tsx
"use client";
import { useState, type ReactNode } from "react";
import { motion, useMotionTemplate, useReducedMotion, useSpring } from "motion/react";
type HoverSpotlightProps = {
/** Stays clickable: the dark layer lets the pointer through. */
children: ReactNode;
/** Size of the light in pixels. */
radius?: number;
/** Colour of the dark around the light. Match your background. */
dim?: string;
className?: string;
};
const FOLLOW = { stiffness: 300, damping: 32 };
/* HoverSpotlight: when the mouse comes in, the block goes dark and only a soft circle around the pointer stays lit. Touch and keyboard see it fully lit. */
export function HoverSpotlight({ children, radius = 140, dim = "rgb(8 7 16 / 0.9)", className = "" }: HoverSpotlightProps) {
const reduce = useReducedMotion();
const [on, setOn] = useState(false);
const x = useSpring(0, FOLLOW);
const y = useSpring(0, FOLLOW);
const size = useSpring(radius * 3, { stiffness: 120, damping: 22 });
const background = useMotionTemplate`radial-gradient(circle ${size}px at ${x}px ${y}px, transparent 50%, ${dim} 100%)`;
const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (reduce || e.pointerType === "touch") return;
const r = e.currentTarget.getBoundingClientRect();
const px = e.clientX - r.left;
const py = e.clientY - r.top;
if (on) {
x.set(px);
y.set(py);
} else {
x.jump(px);
y.jump(py);
size.set(radius);
setOn(true);
}
};
const onLeave = () => {
size.set(radius * 3);
setOn(false);
};
return (
<div onPointerMove={onMove} onPointerLeave={onLeave} className={`relative ${className}`}>
{children}
<motion.div
aria-hidden
className="pointer-events-none absolute inset-0"
initial={false}
animate={{ opacity: on ? 1 : 0 }}
transition={{ duration: 0.35 }}
style={{ background }}
/>
</div>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/hover-spotlight.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/hover-spotlight-demo.tsx
import { HoverSpotlight } from "@/components/ui/hover-spotlight"; const NOTES = ["Plans", "Drafts", "Reviews", "Launches", "Reports", "Archive"]; export default function HoverSpotlightExample() { return ( <HoverSpotlight radius={120} className="h-full overflow-hidden bg-[#0b0a14]"> <div className="grid h-full grid-cols-2 gap-3 p-5 sm:grid-cols-3"> {NOTES.map((note, i) => ( <div key={note} className="rounded-xl border border-white/10 bg-white/5 p-3"> <p className="text-sm font-medium text-white">{note}</p> <p className="mt-1 text-xs text-[#a78bfa]">{(i + 2) * 3} items</p> </div> ))} </div> </HoverSpotlight> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | — | Stays clickable: the dark layer lets the pointer through. |
| radius | number | 140 | Size of the light in pixels. |
| dim | string | "rgb(8 7 16 / 0.9)" | Colour of the dark around the light. Match your background. |
| className | string | "" | On the wrapper. |