Backgrounds
Spotlight
A cursor-following radial glow container for section backgrounds — CSS-var tracking with zero re-renders.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/spotlight.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Spotlight" component — a section
background that paints a soft radial glow following the cursor. Its only
dependency is a cn() class merger (clsx + tailwind-merge).
Contract
- export function Spotlight(props): props extend React.ComponentProps<"div">
(rest props spread onto the root) plus:
- size?: number — spotlight diameter in px (default 320).
- intensity?: "subtle" | "medium" | "bold" (default "medium") — selects a
color-mix alpha tier for the glow: 12% / 20% / 30%.
- children render above the effect layer; className merges onto the root.
- "use client" — the component owns pointer events and reads
prefers-reduced-motion, so it cannot stay a server component.
Behavior
- Root div: relative isolate overflow-hidden, holding a ref that is both
attached to the element and used to write pointer coordinates directly.
- On mousemove, compute the cursor position relative to the root's bounding
rect and write it straight into two CSS custom properties, --spot-x and
--spot-y, via ref.current.style.setProperty — never through React state.
This is the performance contract: a fast mousemove stream updates paint
only, so the component (and everything nested inside it) never re-renders
while the cursor moves.
- A single boolean `active` state (React state is fine here — it only flips
on mouseenter/mouseleave, a low-frequency boundary event) drives a CSS
opacity transition on the light layer: fade in on enter, fade out on
leave.
- The light layer is absolute inset-0, aria-hidden, pointer-events-none,
with a radial-gradient background sized at `${size}px` centered on
`var(--spot-x, 50%) var(--spot-y, 50%)`. The 50%/50% fallback matters: it
is what the effect resolves to whenever the CSS vars are never written.
- prefers-reduced-motion: check window.matchMedia("(prefers-reduced-motion:
reduce)") once in an effect (with a change listener, cleaned up on
unmount) and cache the result in a ref. When reduced motion is on, the
mousemove handler returns early and never writes --spot-x/--spot-y, so the
gradient falls back to a fixed center — the glow stays visible as a static
decoration but stops chasing the cursor.
- Touch / no-hover devices: the light layer carries a
[@media(hover:none)]:hidden class so it is absent by default wherever
there is no cursor to spotlight; content is never affected.
- Cleanup: the reduced-motion media-query change listener is removed on
unmount.
Rendering & styling
- Semantic tokens only. The glow is radial-gradient(${size}px at
var(--spot-x, 50%) var(--spot-y, 50%), color-mix(in oklab, var(--primary)
N%, transparent), transparent 60%) with N from the intensity tier
(12/20/30). No hex / rgb() / oklch() literals — the color adapts to the
host theme and dark mode for free.
- Merge consumer className via cn() on the root div.
- Content sits in a relative z-10 wrapper above the light layer so the glow
can never intercept clicks or enter the accessibility tree.
Customization levers
- Palette: swap var(--primary) for any other theme token (--chart-1..5,
--accent) to change the glow's hue without touching the tracking logic.
- Multiple spotlights: render more than one light-layer div, each with its
own CSS var pair, for a multi-cursor or multi-hotspot effect.
- Size / intensity: size and the ALPHA map are the only two knobs — widen
the 12/20/30 band for a louder or quieter glow.
- Follow inertia: the base version tracks 1:1 with no easing; upgrading to a
spring/lerp follow (e.g. via requestAnimationFrame or a motion library) is
a drop-in swap inside the mousemove handler — the CSS var contract stays
the same.
- Composition: nest GridDots or any other background as a child — Spotlight
only owns the glow layer and the z-10 content slot, so it composes with
whatever pattern sits underneath.Concepts
- Ref-driven CSS variables — pointer coordinates land straight in
--spot-x/--spot-yviastyle.setProperty, skipping React state so a fast mousemove stream never re-renders the children subtree. - Token-derived glow — the light layer's color is
color-mix(in oklab, var(--primary) N%, transparent), so the glow follows the host theme and dark mode automatically; N is 12/20/30 by intensity. - Low-frequency fade — hover enter/leave is the one thing that goes through
useState; it only toggles opacity, so the tracked movement itself never touches render. - Reduced-motion honesty — the CSS vars are never written under
prefers-reduced-motion: reduce(fallbackvar(--spot-x, 50%)), so the glow stays centered and visible instead of chasing a cursor no one asked it to chase. - Touch-safe default —
(hover: none)hides the light layer entirely on touch devices, since there is no cursor to spotlight and no point leaving a gradient stuck mid-screen. - Layer separation — decoration is
aria-hidden pointer-events-none absolute inset-0; content sits in arelative z-10wrapper, so the glow can never intercept clicks or enter the accessibility tree.