Spotlight Cursor
Original · freecursor spotlight mask
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/cur-spotlight.json"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type Star = {
name: string;
constellation: string;
magnitude: string;
distance: string;
note: string;
};
const STARS: Star[] = [
{
name: "Sirius",
constellation: "Canis Major",
magnitude: "mag −1.46",
distance: "8.6 light-years",
note: "The brightest star in the night sky — a hot blue-white sun orbited by the faint white dwarf Sirius B.",
},
{
name: "Betelgeuse",
constellation: "Orion",
magnitude: "mag 0.0–1.3",
distance: "≈ 548 light-years",
note: "A red supergiant so vast it would swallow the orbit of Jupiter. It dimmed dramatically through 2019 and 2020.",
},
{
name: "Rigel",
constellation: "Orion",
magnitude: "mag 0.13",
distance: "≈ 860 light-years",
note: "A blue supergiant roughly 120,000 times more luminous than the Sun, marking the hunter's left foot.",
},
{
name: "Aldebaran",
constellation: "Taurus",
magnitude: "mag 0.86",
distance: "65 light-years",
note: "The orange eye of the bull. It only appears to sit among the Hyades cluster — it lies far in the foreground.",
},
{
name: "Capella",
constellation: "Auriga",
magnitude: "mag 0.08",
distance: "43 light-years",
note: "Actually four stars in two pairs. The brightest golden point riding high overhead on winter evenings.",
},
{
name: "Procyon",
constellation: "Canis Minor",
magnitude: "mag 0.34",
distance: "11.5 light-years",
note: "The 'little dog' star. With Sirius and Betelgeuse it forms the vast Winter Triangle.",
},
];
function mulberry32(seed: number) {
return function next(): number {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
const GRID_CLS = "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3";
function StarCard({ star, dim }: { star: Star; dim: boolean }) {
if (dim) {
return (
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.015] p-4 sm:p-5">
<div className="flex items-baseline justify-between gap-3">
<h3 className="text-lg font-semibold text-slate-600">{star.name}</h3>
<span className="rounded-full bg-white/[0.04] px-2 py-0.5 text-xs font-medium text-slate-700 ring-1 ring-white/[0.05]">
{star.magnitude}
</span>
</div>
<p className="mt-1 text-xs font-medium uppercase tracking-wider text-slate-700">
{star.constellation}
</p>
<p className="mt-2 text-sm text-slate-700">{star.distance}</p>
<p className="mt-3 text-sm leading-relaxed text-slate-700">{star.note}</p>
</div>
);
}
return (
<div className="rounded-2xl border border-indigo-400/25 bg-gradient-to-br from-white/[0.07] to-white/[0.02] p-4 shadow-lg shadow-indigo-950/40 backdrop-blur-sm sm:p-5">
<div className="flex items-baseline justify-between gap-3">
<h3 className="text-lg font-semibold text-white">{star.name}</h3>
<span className="rounded-full bg-amber-400/15 px-2 py-0.5 text-xs font-medium text-amber-300 ring-1 ring-amber-400/30">
{star.magnitude}
</span>
</div>
<p className="mt-1 text-xs font-medium uppercase tracking-wider text-indigo-300">
{star.constellation}
</p>
<p className="mt-2 text-sm text-sky-300">{star.distance}</p>
<p className="mt-3 text-sm leading-relaxed text-slate-300">{star.note}</p>
</div>
);
}
function Switch({
checked,
onChange,
label,
disabled = false,
}: {
checked: boolean;
onChange: (v: boolean) => void;
label: string;
disabled?: boolean;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={label}
disabled={disabled}
onClick={() => onChange(!checked)}
className="group inline-flex items-center gap-2.5 rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 disabled:cursor-not-allowed disabled:opacity-40 dark:focus-visible:ring-offset-neutral-950"
>
<span
className={`relative h-6 w-11 rounded-full transition-colors duration-200 ${
checked ? "bg-indigo-500" : "bg-slate-300 dark:bg-white/15"
}`}
>
<span
className={`absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform duration-200 ${
checked ? "translate-x-5" : "translate-x-0"
}`}
/>
</span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">{label}</span>
</button>
);
}
export default function CurSpotlight() {
const reduced = useReducedMotion();
const stageRef = useRef<HTMLDivElement>(null);
const posRef = useRef<{ x: number; y: number }>({ x: 0.5, y: 0.42 });
const [enabled, setEnabled] = useState(true);
const [auto, setAuto] = useState(false);
const [radius, setRadius] = useState(170);
const dots = useMemo(() => {
const rnd = mulberry32(20260716);
return Array.from({ length: 48 }, () => ({
x: rnd() * 100,
y: rnd() * 100,
s: 1 + rnd() * 2.2,
d: rnd() * 4,
}));
}, []);
const applyPos = useCallback(() => {
const el = stageRef.current;
if (!el) return;
el.style.setProperty("--spot-x", `${posRef.current.x * 100}%`);
el.style.setProperty("--spot-y", `${posRef.current.y * 100}%`);
}, []);
useEffect(() => {
applyPos();
}, [applyPos]);
useEffect(() => {
stageRef.current?.style.setProperty("--spot-r", `${radius}px`);
}, [radius]);
useEffect(() => {
if (!auto || !enabled || reduced) return;
let raf = 0;
let t0: number | null = null;
const loop = (t: number) => {
if (t0 === null) t0 = t;
const e = (t - t0) / 1000;
posRef.current = {
x: clamp(0.5 + Math.cos(e * 0.85) * 0.33, 0, 1),
y: clamp(0.46 + Math.sin(e * 0.85 * 1.35) * 0.3, 0, 1),
};
applyPos();
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [auto, enabled, reduced, applyPos]);
const moveFromClient = (clientX: number, clientY: number) => {
const el = stageRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
posRef.current = {
x: clamp((clientX - r.left) / r.width, 0, 1),
y: clamp((clientY - r.top) / r.height, 0, 1),
};
applyPos();
};
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-slate-100 px-4 py-16 sm:px-6 sm:py-24 dark:from-neutral-950 dark:to-black">
<style>{`
@keyframes curspot-twinkle { 0%, 100% { opacity: .25 } 50% { opacity: .85 } }
@keyframes curspot-halo { 0%, 100% { opacity: .35 } 50% { opacity: .8 } }
.curspot-twinkle { animation: curspot-twinkle 3.6s ease-in-out infinite; }
.curspot-halo { animation: curspot-halo 2.8s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.curspot-twinkle, .curspot-halo { animation: none !important; }
}
`}</style>
<motion.div
initial={reduced ? false : { opacity: 0, y: 14 }}
animate={reduced ? false : { opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="mx-auto w-full max-w-6xl"
>
<div className="max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-500 dark:text-indigo-400">
Interactive star chart
</p>
<h2 className="mt-3 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Sweep the spotlight across the dark
</h2>
<p className="mt-4 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Everything here starts in shadow. Move your cursor — or steer with the keyboard
— and a soft beam reveals each star as it passes. Six real fixtures of the northern
winter sky, waiting to be found.
</p>
</div>
<div className="mt-8 flex flex-wrap items-center gap-x-7 gap-y-4 rounded-2xl border border-slate-200 bg-white/70 p-4 backdrop-blur sm:px-6 dark:border-white/10 dark:bg-white/5">
<Switch label="Spotlight" checked={enabled} onChange={setEnabled} />
<Switch
label="Auto-orbit"
checked={auto}
onChange={setAuto}
disabled={!enabled || !!reduced}
/>
<div className="flex items-center gap-3">
<label
htmlFor="curspot-radius"
className="text-sm font-medium text-slate-700 dark:text-slate-200"
>
Beam size
</label>
<input
id="curspot-radius"
type="range"
min={90}
max={320}
step={5}
value={radius}
disabled={!enabled}
onChange={(e) => setRadius(Number(e.target.value))}
aria-valuetext={`${radius} pixels`}
className="h-1.5 w-36 cursor-pointer appearance-none rounded-full bg-slate-300 accent-indigo-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-white/15 dark:focus-visible:ring-offset-neutral-950"
/>
<span className="w-12 tabular-nums text-sm text-slate-500 dark:text-slate-400">
{radius}px
</span>
</div>
</div>
<div
ref={stageRef}
tabIndex={0}
role="group"
aria-label="Star chart spotlight. Use arrow keys to move the beam of light."
aria-describedby="curspot-hint"
onPointerMove={(e) => {
if (!enabled) return;
if (auto) setAuto(false);
moveFromClient(e.clientX, e.clientY);
}}
onKeyDown={(e) => {
if (!enabled) return;
const step = 0.05;
const p = { ...posRef.current };
let handled = true;
switch (e.key) {
case "ArrowUp":
case "w":
case "W":
p.y -= step;
break;
case "ArrowDown":
case "s":
case "S":
p.y += step;
break;
case "ArrowLeft":
case "a":
case "A":
p.x -= step;
break;
case "ArrowRight":
case "d":
case "D":
p.x += step;
break;
case "Home":
p.x = 0.5;
p.y = 0.5;
break;
case "+":
case "=":
setRadius((r) => clamp(r + 15, 90, 320));
break;
case "-":
case "_":
setRadius((r) => clamp(r - 15, 90, 320));
break;
default:
handled = false;
}
if (!handled) return;
e.preventDefault();
if (auto) setAuto(false);
posRef.current = { x: clamp(p.x, 0, 1), y: clamp(p.y, 0, 1) };
applyPos();
}}
className={`relative isolate mt-8 min-h-[460px] overflow-hidden rounded-3xl border border-white/10 bg-neutral-950 shadow-2xl select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 sm:min-h-[540px] dark:focus-visible:ring-offset-black ${
enabled ? "cursor-crosshair" : "cursor-default"
}`}
>
{/* faint background starfield */}
<div aria-hidden className="pointer-events-none absolute inset-0 z-0">
{dots.map((d, i) => (
<span
key={i}
className="curspot-twinkle absolute rounded-full bg-white"
style={{
left: `${d.x}%`,
top: `${d.y}%`,
width: d.s,
height: d.s,
opacity: 0.5,
animationDelay: `${d.d}s`,
}}
/>
))}
</div>
{/* edge vignette over the unlit layer */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-[5]"
style={{
background:
"radial-gradient(circle at 50% 42%, transparent 38%, rgba(0,0,0,0.55) 100%)",
}}
/>
{/* unlit (dim) content — hidden from assistive tech to avoid duplication */}
<div aria-hidden className="relative z-10 p-5 sm:p-8">
<div className={GRID_CLS}>
{STARS.map((s) => (
<StarCard key={s.name} star={s} dim />
))}
</div>
</div>
{/* lit (vivid) content, masked to the beam — this layer carries the a11y text */}
<div
className="absolute inset-0 z-20 p-5 sm:p-8"
style={
enabled
? {
WebkitMaskImage:
"radial-gradient(circle var(--spot-r) at var(--spot-x) var(--spot-y), #000 0%, #000 58%, rgba(0,0,0,0.35) 78%, transparent 100%)",
maskImage:
"radial-gradient(circle var(--spot-r) at var(--spot-x) var(--spot-y), #000 0%, #000 58%, rgba(0,0,0,0.35) 78%, transparent 100%)",
}
: undefined
}
>
<div className={GRID_CLS}>
{STARS.map((s) => (
<StarCard key={s.name} star={s} dim={false} />
))}
</div>
</div>
{/* additive glow + beam ring */}
{enabled ? (
<>
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-30 mix-blend-screen"
style={{
background:
"radial-gradient(circle var(--spot-r) at var(--spot-x) var(--spot-y), rgba(129,140,248,0.30) 0%, rgba(129,140,248,0.10) 45%, transparent 70%)",
}}
/>
<div
aria-hidden
className="curspot-halo pointer-events-none absolute z-40 -translate-x-1/2 -translate-y-1/2 rounded-full border border-indigo-300/40"
style={{
left: "var(--spot-x)",
top: "var(--spot-y)",
width: "calc(var(--spot-r) * 2)",
height: "calc(var(--spot-r) * 2)",
}}
/>
</>
) : null}
</div>
<p id="curspot-hint" className="mt-3 text-xs leading-relaxed text-slate-500 sm:text-sm dark:text-slate-400">
Focus the panel and steer the beam with the arrow keys or W A S D. Press{" "}
<kbd className="rounded border border-slate-300 bg-white px-1 font-sans text-[0.7em] text-slate-600 dark:border-white/15 dark:bg-white/10 dark:text-slate-300">
+
</kbd>{" "}
/{" "}
<kbd className="rounded border border-slate-300 bg-white px-1 font-sans text-[0.7em] text-slate-600 dark:border-white/15 dark:bg-white/10 dark:text-slate-300">
−
</kbd>{" "}
to resize it, or switch off Spotlight to bring the house lights up.
</p>
</motion.div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Dot Follow Cursor
Originaldot cursor follower

Ring Cursor
Originalring cursor follower

Magnetic Cursor
Originalmagnetic cursor on targets

Trail Cursor
Originalcursor trail effect

Blend Cursor
Originalmix-blend cursor

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

Image Backdrop Hero
OriginalA cinematic full-bleed hero with a layered image-style backdrop, overlaid left-aligned copy and a trusted-by logos strip.

Gradient Mesh Hero with Staggered Text Reveal
OriginalA full-screen hero pairing a drifting animated gradient-mesh backdrop with a word-by-word staggered blur-in headline and a logo marquee under the fold.

