Spotlight Card

A surface with a soft highlight that follows the pointer and fades out on leave.

Loading preview

Installation

npx shadcn@latest add @loomui/spotlight-card

Usage

import { SpotlightCard } from "@/components/ui/spotlight-card"
<SpotlightCard className="bg-card p-5">
  <h3 className="text-sm font-medium">Copy, do not install</h3>
  <p className="text-muted-foreground mt-2 text-sm">
    The source lands in your repo.
  </p>
</SpotlightCard>

Two layers light up, not one. The border ring is masked so only its 1px edge shows, and it does most of the work. The surface wash on its own is too soft to register at a glance.

Pointer position is written to two CSS custom properties inside a requestAnimationFrame, never to React state. Moving the mouse across the card costs zero renders.

Only opacity transitions. The highlight's position updates instantly, because a glow that eases toward the cursor reads as lag rather than polish.

Making the card clickable

interactive gives the card a pointer cursor and a focus ring. Put the real destination on an anchor inside and stretch it over the card. The whole surface becomes the hit target while the accessible name and the href stay on a proper link.

<SpotlightCard interactive className="bg-card p-5">
  <h3 className="text-sm font-medium">
    <a href="/docs/installation" className="after:absolute after:inset-0">
      Copy, do not install
    </a>
  </h3>
  <p className="text-muted-foreground mt-2 text-sm">
    The source lands in your repo.
  </p>
</SpotlightCard>

Reach for interactive on its own only when the card already has a click handler. A pointer cursor on something that does nothing is worse than no cursor change at all.

Props

PropTypeDefaultDescription
sizenumber320Diameter of the highlight in pixels.
colorstring10% currentColorFill of the surface wash. Keep the alpha low.
borderColorstring60% --primaryFill of the border ring. Brighter on purpose.
interactivebooleanfalsePointer cursor and a focus ring on the card.
disabledbooleanfalseRender a plain card with no highlight.
classNamestringnoneMerged onto the card. Border and radius here.

Any other <div> prop is forwarded.

Accessibility

Both layers appear on :focus-within, so a keyboard user reaching a link inside the card gets the same emphasis a mouse user gets. The overlays are aria-hidden and pointer-events-none, and the fade is skipped under prefers-reduced-motion. The card itself is a plain <div>, so it never invents a role it cannot honour.