A container for interactive cards with a hover spotlight effect, ideal for showcasing features or highlights.
import SpotlightCardsContainer, { SpotlightCard } from "@/registry/visual-effects/spotlight-card/spotlight-card";
import cn from "@/utils/cn";
const SpotlightCardPreview = () => {
return (
<SpotlightCardsContainer className="my-5 mx-8 grid sm:grid-cols-2 gap-4 flex-wrap justify-center">
{cards.map((card, cardIndex) => (
<SpotlightCard
spotlightColor="rgb(42, 250, 175)"
spotlightBorderWidth={2}
className={cn(
"rounded-md",
{
"max-sm:hidden": cardIndex >= 2,
},
)}
wrapperProps={{
className: "bg-neutral-50 dark:bg-neutral-800 p-4 shadow shadow-sm",
}}
>
<span className="inline-block mb-3 text-xl">
{card.icon}
</span>
<h2 className="text-md font-bold text-neutral-900 dark:text-white mb-1">
{card.title}
</h2>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{card.description}
</p>
</SpotlightCard>
))}
</SpotlightCardsContainer>
);
};
const cards = [
{
icon: "🎨",
title: "Customizable UI",
description: "Easily adapt components to match your brand and design system.",
},
{
icon: "📱",
title: "Responsive Design",
description: "Optimized layouts that look great on mobile, tablet, and desktop screens.",
},
{
icon: "🧑💻",
title: "Developer Friendly",
description: "Simple APIs and flexible props make development fast and enjoyable.",
},
{
icon: "📈",
title: "Scalable Architecture",
description: "Designed to grow with your application from small projects to large systems.",
},
];
export default SpotlightCardPreview; Install the following packages before using this component.
import { memo, useMemo } from "react";
import cn from "@/utils/cn";
export type SpotlightCardsContainerProps = (
React.ComponentProps<"div">
);
export type SpotlightCardProps = {
spotlightColor: string;
spotlightSize?: number;
spotlightBorderWidth?: number,
wrapperProps: React.ComponentProps<"div">,
} & React.ComponentProps<"div">;
const spotlightCardClass = "spotlight-card";
const SpotlightCardsContainer = (
props: SpotlightCardsContainerProps,
) => {
const {
children,
className,
...restProps
} = props;
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const { clientX, clientY, currentTarget } = e;
const cards = currentTarget.querySelectorAll<HTMLDivElement>(`.${spotlightCardClass}`);
cards.forEach((card) => {
const { x, y } = card.getBoundingClientRect();
card.style.setProperty("--mx", `${clientX - x}px`);
card.style.setProperty("--my", `${clientY - y}px`);
});
};
return (
<div
{...restProps}
className={cn(
"group",
className,
)}
onMouseMove={handleMouseMove}
>
{children}
</div>
);
};
export const SpotlightCard = memo((
props: SpotlightCardProps
) => {
const {
children,
spotlightColor = "rgb(127, 127, 127)",
spotlightSize = 100,
spotlightBorderWidth = 1,
className,
style,
wrapperProps = {},
...restProps
} = props;
const {
className: wrapperClassName = "",
...restWrapperProps
} = wrapperProps;
const _spotlightBorderWidth = useMemo(() => (
Math.max(0, spotlightBorderWidth)
), [spotlightBorderWidth]);
const _spotlightColor = useMemo(() => {
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
ctx.fillStyle = spotlightColor;
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
canvas.remove();
return (alpha: number) => {
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
}, [spotlightColor]);
return (
<div
{...restProps}
className={cn(
"relative",
"[padding:var(--spotlight-border-width)]",
"before:content-[''] before:absolute before:inset-0 before:z-[1] before:pointer-events-none before:opacity-[0] before:rounded-[inherit] before:transition-all before:duration-150 before:ease-in-out",
"after:content-[''] after:absolute after:inset-0 after:z-[3] after:pointer-events-none after:opacity-[0] after:rounded-[inherit] after:transition-all before:duration-150 before:ease-in-out",
"before:bg-[radial-gradient(var(--spotlight-size)_circle_at_var(--mx)_var(--my),var(--spotlight-before-color),rgba(0,0,0,0))]",
"after:bg-[radial-gradient(var(--spotlight-size)_circle_at_var(--mx)_var(--my),var(--spotlight-after-color),rgba(0,0,0,0))]",
"group-hover:before:opacity-[1]",
"hover:after:opacity-100",
className,
spotlightCardClass,
)}
style={{
...style,
"--spotlight-size": `${spotlightSize}px`,
"--spotlight-before-color": _spotlightColor(0.8),
"--spotlight-after-color": _spotlightColor(0.2),
"--spotlight-border-width": `${_spotlightBorderWidth}px`,
} as React.CSSProperties}
>
<div
{...restWrapperProps}
className={cn(
"relative rounded-[inherit] z-[2] h-full",
wrapperClassName,
)}
>
{children}
</div>
</div>
);
});
export default memo(SpotlightCardsContainer); | Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | React.ReactNode | Yes | — | One or more SpotlightCard components to be rendered inside the container. |
| className | string | No | — | Additional CSS classes applied to the container. |
| style | React.CSSProperties | No | — | Inline styles applied to the container element. |
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | React.ReactNode | Yes | — | Content to be rendered inside the card. |
| spotlightColor | string | No | "rgb(127, 127, 127)" | Spotlight color. Accepts RGB or HEX; alpha channel is used to control spotlight intensity. |
| spotlightSize | number | No | 100 | Size of the spotlight effect in pixels. |
| spotlightBorderWidth | number | No | 1 | Width of the card border in pixels; adopts spotlight color on hover proximity. |
| className | string | No | — | Additional CSS classes applied to the card. |
| style | React.CSSProperties | No | — | Inline styles applied to the card. |
| wrapperProps | React.ComponentProps<"div"> | No | {} | Props passed to the wrapper element (e.g., className, style, etc.). |
If you find this component useful, consider starring the repository on GitHub. Found a bug or have a suggestion? Open an issue to help improve it.