# Spotlight Card

> Card with a spotlight that follows the pointer and a glowing border.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/spotlight-card
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/spotlight-card.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/spotlight-card-demo.tsx"
import { Gauge, ShieldCheck, Sparkles } from "lucide-react"

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

const features = [
  { icon: Gauge, title: "Fast by default", text: "Server-rendered and streamed, with zero layout shift." },
  { icon: ShieldCheck, title: "Secure", text: "Sandboxed previews and audited dependencies." },
  { icon: Sparkles, title: "Delightful", text: "Motion that feels natural and respects your settings." },
]

export default function SpotlightCardDemo() {
  return (
    <div className="grid w-full max-w-4xl gap-4 sm:grid-cols-3">
      {features.map(({ icon: Icon, title, text }) => (
        <SpotlightCard key={title}>
          <Icon className="size-6 text-orange-500" />
          <h3 className="mt-4 font-medium">{title}</h3>
          <p className="mt-1.5 text-sm text-muted-foreground">{text}</p>
        </SpotlightCard>
      ))}
    </div>
  )
}
```

## Source

```tsx title="components/spotlight-card.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

export interface SpotlightCardProps extends React.ComponentProps<"div"> {
  spotlightColor?: string
  /** Diameter of the spotlight in px */
  size?: number
  /** Also light up the card border near the pointer */
  glowBorder?: boolean
}

// The pointer position is written to CSS variables, so moving the mouse
// doesn't re-render React.
export function SpotlightCard({
  spotlightColor = "#f97316",
  size = 360,
  glowBorder = true,
  className,
  children,
  onPointerMove,
  ...props
}: SpotlightCardProps) {
  const ref = React.useRef<HTMLDivElement>(null)

  const move = (event: React.PointerEvent<HTMLDivElement>) => {
    const el = ref.current
    if (el) {
      const rect = el.getBoundingClientRect()
      el.style.setProperty("--x", `${event.clientX - rect.left}px`)
      el.style.setProperty("--y", `${event.clientY - rect.top}px`)
    }
    onPointerMove?.(event)
  }

  const radius = `${size / 2}px`
  return (
    <div
      ref={ref}
      onPointerMove={move}
      className={cn(
        "group/spotlight relative overflow-hidden rounded-2xl border bg-card p-6 text-card-foreground shadow-sm [--x:50%] [--y:-200px]",
        className
      )}
      {...props}
    >
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover/spotlight:opacity-100"
        style={{
          background: `radial-gradient(${radius} circle at var(--x) var(--y), color-mix(in oklab, ${spotlightColor} 18%, transparent), transparent 70%)`,
        }}
      />
      {glowBorder && (
        <div
          aria-hidden
          className="pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent opacity-0 transition-opacity duration-300 group-hover/spotlight:opacity-100"
          style={{
            background: `radial-gradient(${radius} circle at var(--x) var(--y), ${spotlightColor}, transparent 60%) border-box`,
            mask: "linear-gradient(#000 0 0) padding-box exclude, linear-gradient(#000 0 0) border-box",
          }}
        />
      )}
      <div className="relative">{children}</div>
    </div>
  )
}
```
