Cards & Layoutv0.1.0
Spotlight Card
A radial glow follows the cursor across the surface.
Installation
CLI
npx shadcn@latest add https://hantui-theta.vercel.app/r/spotlight-card.jsonManual
Install the dependencies
npm install clsx motion tailwind-mergeAdd the shared helpers
Components use
cn()fromlib/utils.ts. See Installation if you have not set them up yet.Copy the source
components/ui/spotlight-card/spotlight-card.tsx"use client"; import type { ComponentProps, PointerEvent } from "react"; import { motion, useMotionValue, useSpring } from "motion/react"; import { cn } from "@/lib/utils"; export interface SpotlightCardProps extends Omit<ComponentProps<"div">, "onAnimationStart" | "onDrag" | "onDragStart" | "onDragEnd"> { /** Glow color across the surface. @default "color-mix(in oklab, var(--primary) 14%, transparent)" */ color?: string; /** Glow color along the border. @default "color-mix(in oklab, var(--primary) 70%, transparent)" */ borderColor?: string; /** Glow diameter in px. @default 380 */ size?: number; /** Light up the border near the cursor too. @default true */ border?: boolean; } const FADE = { stiffness: 260, damping: 30 }; // Keeps only a 1px ring at the edge: the content box is cut out of the full box. const RING_MASK = { padding: 1, mask: "linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)", WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)", WebkitMaskComposite: "xor", } as const; export function SpotlightCard({ color = "color-mix(in oklab, var(--primary) 14%, transparent)", borderColor = "color-mix(in oklab, var(--primary) 70%, transparent)", size = 380, border = true, className, children, onPointerMove, onPointerLeave, ...props }: SpotlightCardProps) { const x = useMotionValue(0); const y = useMotionValue(0); const presence = useMotionValue(0); const opacity = useSpring(presence, FADE); // The glow is a fixed-size circle moved with transforms; nothing repaints a gradient per frame. const glow = (background: string) => ( <motion.span className="absolute block rounded-full" style={{ width: size, height: size, left: -size / 2, top: -size / 2, x, y, opacity, background: `radial-gradient(circle, ${background}, transparent 70%)` }} /> ); return ( <div className={cn("relative overflow-hidden", className)} onPointerMove={(event: PointerEvent<HTMLDivElement>) => { onPointerMove?.(event); if (event.pointerType !== "mouse") return; const rect = event.currentTarget.getBoundingClientRect(); x.set(event.clientX - rect.left); y.set(event.clientY - rect.top); presence.set(1); }} onPointerLeave={(event: PointerEvent<HTMLDivElement>) => { onPointerLeave?.(event); presence.set(0); }} {...props} > {/* Pointer-driven only (it never moves on its own), so reduced motion needs no special case. */} <span aria-hidden="true" className="pointer-events-none absolute inset-0"> {glow(color)} </span> {border ? ( <span aria-hidden="true" className="pointer-events-none absolute inset-0 rounded-[inherit]" style={RING_MASK}> {glow(borderColor)} </span> ) : null} <div className="relative">{children}</div> </div> ); }
Usage
import { SpotlightCard } from "@/components/ui/spotlight-card/spotlight-card";
export function Feature() {
return (
<SpotlightCard className="rounded-2xl border border-border bg-card p-5">
<h3>Fast by default</h3>
</SpotlightCard>
);
}API reference
| Prop | Type | Default | Description |
|---|---|---|---|
color | string | "color-mix(in oklab, var(--primary) 14%, transparent)" | Glow color across the surface. |
borderColor | string | "color-mix(in oklab, var(--primary) 70%, transparent)" | Glow color along the border. |
size | number | 380 | Glow diameter in px. |
border | boolean | true | Light up the border near the cursor too. |
Also accepts every native <div> attribute, including ref and className.
- The glow is a fixed-size radial circle moved with transforms; no gradient is repainted per frame.
- The border light is the same glow behind a mask that keeps only a 1px ring, so the edge lights up near the cursor.
- Use `color` and `borderColor` for dark cards (see the Dark example).
Accessibility
- Purely decorative layers (aria-hidden); content and semantics are untouched.
- The glow only follows a mouse. It's driven by the pointer and never moves on its own, so it stays on under reduced motion.