Spotlight card
A card with a soft radial glow that follows the pointer and rests centred on touch.
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
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | Heading at the top of the card. | |
| description | string | One line under the title. | |
| radius | number | 240 | Radius of the glow in pixels. |
| children | ReactNode | Anything rendered under the title and description. |
Source
"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