Card Spotlight Hover Effect
Original · freeA card with a cursor-following radial spotlight.
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/hover-card-spotlight.json"use client";
import { type PointerEvent, useCallback, useRef, useState } from "react";
import { useReducedMotion } from "motion/react";
type SpotlightCard = {
eyebrow: string;
title: string;
body: string;
meta: string;
href: string;
};
const CARDS: SpotlightCard[] = [
{
eyebrow: "Edge runtime",
title: "Ship globally in one deploy",
body: "Push once and your functions run in 34 regions, milliseconds from every visitor. No config, no cold-start tax.",
meta: "34 regions",
href: "#edge",
},
{
eyebrow: "Observability",
title: "Trace every request end to end",
body: "Structured logs, spans and flame graphs stitched together automatically. Find the slow span before your users do.",
meta: "p99 < 40ms",
href: "#traces",
},
{
eyebrow: "Security",
title: "Secrets that never touch disk",
body: "Encrypted at rest with per-environment scoping and full audit trails. Rotate a key and every replica updates live.",
meta: "SOC 2 II",
href: "#secrets",
},
];
export default function HoverCardSpotlight() {
const reduce = useReducedMotion();
return (
<section className="relative w-full overflow-hidden bg-zinc-50 px-6 py-20 text-zinc-900 sm:py-28 dark:bg-zinc-950 dark:text-zinc-100">
<style>{`
@keyframes hcs-rise {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.hcs-rise { animation: hcs-rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; }
@media (prefers-reduced-motion: reduce) {
.hcs-rise { animation: none; }
}
`}</style>
{/* ambient backdrop */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-60 [background-image:radial-gradient(circle_at_1px_1px,theme(colors.zinc.300)_1px,transparent_0)] [background-size:22px_22px] dark:opacity-40 dark:[background-image:radial-gradient(circle_at_1px_1px,theme(colors.zinc.800)_1px,transparent_0)]"
/>
<div className="relative mx-auto max-w-6xl">
<header className="mx-auto mb-14 max-w-2xl text-center">
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
Platform primitives
</p>
<h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
Move your cursor across the cards
</h2>
<p className="mt-4 text-pretty text-base leading-relaxed text-zinc-600 dark:text-zinc-400">
A radial spotlight follows the pointer and reveals a fine border glow,
focusable by keyboard and gentle when motion is reduced.
</p>
</header>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{CARDS.map((card, i) => (
<Spotlight key={card.title} card={card} index={i} reduce={!!reduce} />
))}
</div>
</div>
</section>
);
}
function Spotlight({
card,
index,
reduce,
}: {
card: SpotlightCard;
index: number;
reduce: boolean;
}) {
const ref = useRef<HTMLAnchorElement>(null);
const [active, setActive] = useState(false);
const move = useCallback((clientX: number, clientY: number) => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
el.style.setProperty("--hcs-x", `${clientX - rect.left}px`);
el.style.setProperty("--hcs-y", `${clientY - rect.top}px`);
}, []);
const onPointerMove = useCallback(
(e: PointerEvent<HTMLAnchorElement>) => {
if (reduce) return;
move(e.clientX, e.clientY);
},
[move, reduce]
);
const centerSpot = useCallback(() => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
el.style.setProperty("--hcs-x", `${rect.width / 2}px`);
el.style.setProperty("--hcs-y", `${rect.height * 0.4}px`);
}, []);
return (
<a
ref={ref}
href={card.href}
onPointerMove={onPointerMove}
onPointerEnter={() => setActive(true)}
onPointerLeave={() => setActive(false)}
onFocus={() => {
centerSpot();
setActive(true);
}}
onBlur={() => setActive(false)}
style={{ animationDelay: reduce ? undefined : `${index * 90}ms` }}
className={[
"hcs-rise group relative block overflow-hidden rounded-2xl p-px",
"outline-none transition-transform duration-300 ease-out",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-50 dark:focus-visible:ring-offset-zinc-950",
reduce ? "" : "hover:-translate-y-1",
].join(" ")}
>
{/* border glow layer — follows cursor */}
<span
aria-hidden
className="pointer-events-none absolute inset-0 rounded-2xl bg-zinc-200 transition-opacity duration-300 dark:bg-zinc-800"
style={{
opacity: active ? 1 : 0,
background:
"radial-gradient(220px circle at var(--hcs-x, 50%) var(--hcs-y, 50%), rgb(99 102 241 / 0.55), transparent 65%)",
}}
/>
{/* card face */}
<div className="relative h-full rounded-[15px] bg-white p-6 dark:bg-zinc-900">
{/* inner spotlight wash */}
<span
aria-hidden
className="pointer-events-none absolute inset-0 rounded-[15px] transition-opacity duration-300"
style={{
opacity: active ? 1 : 0,
background:
"radial-gradient(260px circle at var(--hcs-x, 50%) var(--hcs-y, 50%), rgb(99 102 241 / 0.12), transparent 60%)",
}}
/>
<div className="relative flex h-full flex-col">
<div className="mb-5 flex items-center justify-between">
<span className="inline-flex items-center rounded-full bg-indigo-50 px-2.5 py-1 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-500/25">
{card.eyebrow}
</span>
<span className="font-mono text-xs tabular-nums text-zinc-400 dark:text-zinc-500">
{card.meta}
</span>
</div>
<h3 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
{card.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
{card.body}
</p>
<span className="mt-6 inline-flex items-center gap-1.5 text-sm font-medium text-indigo-600 dark:text-indigo-400">
Learn more
<svg
viewBox="0 0 16 16"
fill="none"
aria-hidden
className="h-3.5 w-3.5 transition-transform duration-300 group-hover:translate-x-1"
>
<path
d="M3 8h9M8.5 4l4 4-4 4"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</div>
</div>
</a>
);
}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 →
Card Lift Hover Effect
OriginalA card that lifts with a growing shadow on hover.

Card Glow Hover Effect
OriginalA card with a coloured glow that blooms on hover.

Card Border Draw Hover Effect
OriginalA card whose border draws itself in on hover.

Card Gradient Shift Hover Effect
OriginalA card whose gradient background shifts on hover.

Card Tilt Hover Effect
OriginalA card that tilts in 3D toward the cursor on hover.

Link Underline Hover Effect
OriginalAnimated link underline effects: grow, slide and centre-out.

Link Slide Hover Effect
OriginalLinks whose label slides up to a duplicate on hover.

Button Fill Hover Effect
OriginalButtons with a colour fill that sweeps across on hover.

Button Shine Hover Effect
OriginalButtons with a diagonal shine sweep on hover.
Icon Fill Hover Effect
OriginalIcon buttons whose icon and background fill on hover.
Icon Bounce Hover Effect
OriginalIcons that bounce or spin playfully on hover.

List Highlight Hover Effect
OriginalA list where the hovered row gets a sliding highlight and arrow.

