Skip to content
goodtabs UI
BrowseBrowse components

Spotlight card

A card with a soft radial glow that follows the pointer and rests centred on touch.

npx shadcn@latest add https://ui.goodtabs.co/r/spotlight-card.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/spotlight-card

Instant search

Every resource is indexed on build, so results land on the first keystroke.

One card with a title and a body. Move the pointer over it.

A grid of three

Curated

Every listing is picked by a person and written in one honest line.

Keyboard first

Cmd K opens search, j and k move, s saves, Enter opens the tab.

Static and fast

Pages are generated from JSON at build time. No spinners.

Feature cards in a grid, each with its own light.

Custom content

312

Saved this month

Synced across two devices

No title or description, just children and a smaller radius.

Usage

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

<SpotlightCard title="Instant search" description="Results land on the first keystroke." />

Props

PropTypeDefaultDescription
titlestringHeading at the top of the card.
descriptionstringOne line under the title.
radiusnumber240Radius of the glow in pixels.
childrenReactNodeAnything rendered under the title and description.

Source

spotlight-card/spotlight-card.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

type SpotlightCardProps = React.ComponentProps<"div"> & {
  /** Heading at the top of the card. */
  title?: string;
  /** One line under the title. */
  description?: string;
  /** Radius of the glow in pixels. */
  radius?: number;
};

const reducedMotion = () =>
  typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

/**
 * A card with a soft radial glow that follows the pointer.
 * On touch and under reduced motion the glow stays centred and faint, so the card still reads as lit.
 */
export function SpotlightCard({
  title,
  description,
  radius = 240,
  className,
  style,
  children,
  onPointerMove,
  onPointerEnter,
  onPointerLeave,
  ...props
}: SpotlightCardProps) {
  const [active, setActive] = React.useState(false);

  const follows = (event: React.PointerEvent<HTMLDivElement>) => event.pointerType === "mouse" && !reducedMotion();

  const handleMove = (event: React.PointerEvent<HTMLDivElement>) => {
    onPointerMove?.(event);
    if (!follows(event)) return;
    const rect = event.currentTarget.getBoundingClientRect();
    event.currentTarget.style.setProperty("--spotlight-x", `${event.clientX - rect.left}px`);
    event.currentTarget.style.setProperty("--spotlight-y", `${event.clientY - rect.top}px`);
  };

  const handleEnter = (event: React.PointerEvent<HTMLDivElement>) => {
    onPointerEnter?.(event);
    if (follows(event)) setActive(true);
  };

  const handleLeave = (event: React.PointerEvent<HTMLDivElement>) => {
    onPointerLeave?.(event);
    setActive(false);
  };

  return (
    <div
      data-slot="spotlight-card"
      data-active={active || undefined}
      onPointerMove={handleMove}
      onPointerEnter={handleEnter}
      onPointerLeave={handleLeave}
      style={{ "--spotlight-radius": `${radius}px`, ...style } as React.CSSProperties}
      className={cn(
        "surface-shine relative isolate overflow-hidden rounded-lg border bg-card p-5 text-card-foreground [--spotlight-x:50%] [--spotlight-y:50%]",
        "before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:opacity-0 before:transition-opacity before:duration-150 before:ease-out before:content-['']",
        "before:bg-[radial-gradient(var(--spotlight-radius)_circle_at_var(--spotlight-x)_var(--spotlight-y),var(--glow),transparent_70%)]",
        "motion-reduce:before:opacity-60 pointer-coarse:before:opacity-60 data-active:before:opacity-100",
        className,
      )}
      {...props}
    >
      {title && <h3 className="m-0 text-sm font-medium text-foreground">{title}</h3>}
      {description && <p className={cn("m-0 text-sm text-muted-foreground", title && "mt-1")}>{description}</p>}
      {children}
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/spotlight-card/prompt.md, or the Copy AI prompt button above.

When to use

  • Feature grids on a landing page where a little light rewards the pointer
  • A handful of cards that should feel alive without any colour
  • Anywhere a plain card would do but the page has room for one quiet effect

When not to use

  • Dense lists and tables, where a moving glow distracts from scanning
  • Cards that are mostly interactive controls; the glow adds nothing to a form
  • When the consumer theme has no --glow token, since the light would be invisible

Agent notes

  • Client component, but it takes no function props, so server components can render it directly
  • The glow is a before pseudo element painted with the --glow token; it fades in over 150ms on a mouse pointer and sets no state on move
  • On touch and under prefers-reduced-motion the glow stays centred at 60% opacity, so the still state reads on its own
  • Content is plain children under the optional title and description; the card does not make itself a link or button