# Spotlight Card

> Card whose border and surface light up under the cursor.

Category: Effects. Docs: https://get-neonkit.vercel.app/components/spotlight-card

## Installation

```bash
npx shadcn@latest add https://get-neonkit.vercel.app/r/spotlight-card.json
```

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/spotlight-card`.

npm dependencies: none. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { SpotlightCard } from "@/components/ui/spotlight-card"

<SpotlightCard className="p-6">Hover me</SpotlightCard>
```

## Examples

### Feature grid

```tsx
import { Gauge, Layers, ShieldCheck } from "lucide-react";
import { SpotlightCard } from "@/components/ui/spotlight-card";

const features = [
  { icon: Gauge, title: "Edge cached", body: "Static pages ship from 300 locations. Median TTFB 41ms." },
  { icon: ShieldCheck, title: "Signed builds", body: "Every artifact is checksummed and traceable to a commit." },
  { icon: Layers, title: "Preview per branch", body: "Push a branch, get a URL. Delete the branch, it's gone." },
];

export default function SpotlightCardDemo() {
  return (
    <div className="grid w-full max-w-3xl gap-3 sm:grid-cols-3">
      {features.map(({ icon: Icon, title, body }) => (
        <SpotlightCard key={title} className="p-5">
          <Icon className="size-5 text-white/60" />
          <h3 className="mt-6 text-sm font-medium text-white/90">{title}</h3>
          <p className="mt-1.5 text-sm leading-relaxed text-white/50">{body}</p>
        </SpotlightCard>
      ))}
    </div>
  );
}
```

### Wider light

radius sets the size of the pool of light.

```tsx
import { SpotlightCard } from "@/components/ui/spotlight-card";

export default function SpotlightCardProfile() {
  return (
    <SpotlightCard color="violet" radius={480} className="w-full max-w-sm p-6">
      <p className="font-mono text-xs uppercase tracking-[0.2em] text-white/40">Now playing</p>
      <p className="mt-4 text-2xl font-medium tracking-tight text-white/90">Midnight City</p>
      <p className="text-sm text-white/50">M83 · Hurry Up, We&apos;re Dreaming</p>
      <div className="mt-6 flex items-center gap-3 text-xs tabular-nums text-white/40">
        <span>1:42</span>
        <div className="h-1 flex-1 rounded-full bg-white/10">
          <div className="h-full w-[42%] rounded-full bg-[rgb(168_85_247)] shadow-[0_0_10px_rgb(168_85_247/0.7)]" />
        </div>
        <span>4:03</span>
      </div>
    </SpotlightCard>
  );
}
```

## API reference

### SpotlightCard

Also accepts every div attribute.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `NeonColor` | `inherited` | Light color. |
| `radius` | `number` | `360` | Diameter of the light in pixels. |

## Accessibility

- The pointer position is written to CSS variables directly, so moving the cursor never re-renders React.
- The light also appears when something inside the card has keyboard focus.

## Source

`components/ui/spotlight-card.tsx`

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

export interface SpotlightCardProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
  /** Palette name or any hex color. Inherits `--neon-accent` when omitted. */
  color?: NeonColor;
  /** Diameter of the light in pixels. */
  radius?: number;
}

const SpotlightCard = React.forwardRef<HTMLDivElement, SpotlightCardProps>(
  (
    { className, style, color, radius = 360, children, onPointerMove, ...props },
    forwardedRef,
  ) => {
    const ref = React.useRef<HTMLDivElement>(null);
    React.useImperativeHandle(forwardedRef, () => ref.current as HTMLDivElement);

    // Write the pointer position straight to CSS variables: no re-renders.
    const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
      onPointerMove?.(event);
      const node = ref.current;
      if (!node) return;
      const rect = node.getBoundingClientRect();
      node.style.setProperty("--x", `${event.clientX - rect.left}px`);
      node.style.setProperty("--y", `${event.clientY - rect.top}px`);
    };

    return (
      <div
        ref={ref}
        onPointerMove={handlePointerMove}
        style={neonVars(color, { "--r": `${radius / 2}px`, ...style } as React.CSSProperties)}
        className={cn(
          "[--neon:var(--neon-accent,34_211_238)] [--x:50%] [--y:50%]",
          "group/spotlight relative isolate overflow-hidden rounded-2xl border border-white/10 bg-white/[0.03]",
          className,
        )}
        {...props}
      >
        {/* Light on the surface */}
        <div
          aria-hidden
          className="pointer-events-none absolute inset-0 -z-10 opacity-0 transition-opacity duration-300 group-hover/spotlight:opacity-100 group-focus-within/spotlight:opacity-100 [background:radial-gradient(var(--r)_circle_at_var(--x)_var(--y),rgb(var(--neon)/0.12),transparent_70%)]"
        />
        {/* Light caught by the border */}
        <div
          aria-hidden
          className="pointer-events-none absolute inset-0 rounded-[inherit] p-px opacity-0 transition-opacity duration-300 group-hover/spotlight:opacity-100 group-focus-within/spotlight:opacity-100 [background:radial-gradient(calc(var(--r)*0.75)_circle_at_var(--x)_var(--y),rgb(var(--neon)),transparent_70%)] [mask:linear-gradient(#000_0_0)_content-box_exclude,linear-gradient(#000_0_0)]"
        />
        {children}
      </div>
    );
  },
);
SpotlightCard.displayName = "SpotlightCard";

export { SpotlightCard };
```
