A card with a soft radial glow that tracks the cursor across its surface on hover-capable devices.
npx shadcn@latest add @paragon/spotlight-card"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface SpotlightCardProps extends React.ComponentProps<"div"> {
/** Diameter of the glow in px. */
size?: number;
/** Glow color. Defaults to a theme-aware wash of the primary color. */
color?: string;
/** Renders a plain card with no cursor-tracking glow. */
static?: boolean;
}
/**
* A card with a soft radial glow that tracks the cursor across its surface.
*
* The pointer position is written as CSS custom properties directly on the
* overlay element — never on the parent, which would recalculate styles for
* every descendant on each move. Tracking therefore costs one gradient
* repaint and zero React renders. The overlay is decorative
* (`aria-hidden`, `pointer-events-none`) and only becomes visible on
* hover-capable fine-pointer devices; touch gets a plain card.
*/
export function SpotlightCard({
size = 360,
color = "color-mix(in oklab, var(--primary) 8%, transparent)",
static: isStatic = false,
className,
children,
onPointerMove,
...props
}: SpotlightCardProps) {
const glowRef = React.useRef<HTMLDivElement>(null);
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
onPointerMove?.(event);
const glow = glowRef.current;
if (!glow || event.pointerType === "touch") return;
const rect = event.currentTarget.getBoundingClientRect();
glow.style.setProperty("--spotlight-x", `${event.clientX - rect.left}px`);
glow.style.setProperty("--spotlight-y", `${event.clientY - rect.top}px`);
};
return (
<div
data-slot="spotlight-card"
className={cn(
"group/spotlight relative overflow-hidden rounded-xl bg-card p-6 text-card-foreground shadow-border",
className,
)}
onPointerMove={isStatic ? onPointerMove : handlePointerMove}
{...props}
>
{!isStatic && (
<div
ref={glowRef}
aria-hidden
className="pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-(--duration-base) ease-out [@media(hover:hover)_and_(pointer:fine)]:group-hover/spotlight:opacity-100"
style={{
background: `radial-gradient(${size}px circle at var(--spotlight-x, 50%) var(--spotlight-y, 40%), ${color}, transparent 70%)`,
}}
/>
)}
{children}
</div>
);
}