Spotlight Card
CardsA surface that reveals a soft radial spotlight tracking the cursor. Pure CSS variables — zero re-renders per mouse move, no animation library.
Move your cursor
A soft emerald spotlight tracks the pointer across the surface — driven entirely by CSS variables, so it never re-renders React.
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface SpotlightCardProps extends React.ComponentProps<"div"> {
/** Spotlight radius in pixels. */
radius?: number;
/** Spotlight color (any CSS color). Defaults to the emerald accent. */
color?: string;
}
/**
* A surface that reveals a soft radial spotlight following the cursor.
* Pure CSS variables — no animation library, no re-render per mouse move.
*/
export function SpotlightCard({
children,
className,
radius = 350,
color = "color-mix(in oklch, var(--accent) 22%, transparent)",
...props
}: SpotlightCardProps) {
const ref = React.useRef<HTMLDivElement>(null);
const [active, setActive] = React.useState(false);
const onMouseMove = React.useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
el.style.setProperty("--x", `${e.clientX - rect.left}px`);
el.style.setProperty("--y", `${e.clientY - rect.top}px`);
}, []);
return (
<div
ref={ref}
onMouseMove={onMouseMove}
onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)}
data-slot="spotlight-card"
className={cn(
"group relative overflow-hidden rounded-base border border-border bg-surface p-6",
className,
)}
style={
{
"--spotlight-size": `${radius}px`,
"--spotlight-color": color,
} as React.CSSProperties
}
{...props}
>
<div
aria-hidden
className="pointer-events-none absolute -inset-px z-0 transition-opacity duration-300"
style={{
opacity: active ? 1 : 0,
background:
"radial-gradient(var(--spotlight-size) circle at var(--x) var(--y), var(--spotlight-color), transparent 70%)",
}}
/>
<div className="relative z-10">{children}</div>
</div>
);
}Overview
A surface that reveals a soft radial spotlight tracking the cursor. It is a great way to add depth and interactivity to feature cards, pricing tiers, or empty states without reaching for a heavy animation library.
How it works
The effect is driven entirely by CSS custom properties. On mouse move we write
the pointer position to --x / --y on the element and let a radial-gradient
do the rest — React never re-renders, so it stays smooth even with many cards on
screen.
Because there's no per-frame React state, you can drop dozens of these on a page without a performance hit.
Customizing
Tune the radius prop for a tighter or wider glow, and pass any CSS color via
color — including color-mix() expressions — to match a section's accent.
Accessibility
The spotlight layer is purely decorative and marked aria-hidden, so it never
interferes with screen readers or keyboard navigation of the content inside.
Installation
npx shadcn@latest add https://ui.saumyarex.xyz/r/spotlight-card.json1. Install dependencies
npm install clsx tailwind-merge2. Copy the source into your project
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface SpotlightCardProps extends React.ComponentProps<"div"> {
/** Spotlight radius in pixels. */
radius?: number;
/** Spotlight color (any CSS color). Defaults to the emerald accent. */
color?: string;
}
/**
* A surface that reveals a soft radial spotlight following the cursor.
* Pure CSS variables — no animation library, no re-render per mouse move.
*/
export function SpotlightCard({
children,
className,
radius = 350,
color = "color-mix(in oklch, var(--accent) 22%, transparent)",
...props
}: SpotlightCardProps) {
const ref = React.useRef<HTMLDivElement>(null);
const [active, setActive] = React.useState(false);
const onMouseMove = React.useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
el.style.setProperty("--x", `${e.clientX - rect.left}px`);
el.style.setProperty("--y", `${e.clientY - rect.top}px`);
}, []);
return (
<div
ref={ref}
onMouseMove={onMouseMove}
onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)}
data-slot="spotlight-card"
className={cn(
"group relative overflow-hidden rounded-base border border-border bg-surface p-6",
className,
)}
style={
{
"--spotlight-size": `${radius}px`,
"--spotlight-color": color,
} as React.CSSProperties
}
{...props}
>
<div
aria-hidden
className="pointer-events-none absolute -inset-px z-0 transition-opacity duration-300"
style={{
opacity: active ? 1 : 0,
background:
"radial-gradient(var(--spotlight-size) circle at var(--x) var(--y), var(--spotlight-color), transparent 70%)",
}}
/>
<div className="relative z-10">{children}</div>
</div>
);
}import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/** Merge conditional class names and resolve Tailwind conflicts. */
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
radius | number | 350 | Spotlight radius in pixels. |
color | string | emerald accent | Any CSS color for the spotlight glow. |
...props | React.ComponentProps<"div"> | — | All native div attributes are forwarded. |