Spotlight
Add a light that follows the pointer across a surface and stays visible on keyboard focus.
import { SpotlightGroup, SpotlightSurface } from '@youj/motion'
Installation & usage
npm i @youj/motionFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use it on surfaces people choose between: plans, integrations, templates. The light shows which one the pointer is on, and a group shows how close the neighbors are. Don't light surfaces that are read rather than chosen, like body text or tables, and don't light every card on a page. For the ready-made card look, use Spotlight Card.
SpotlightSurface is a surface with the lit layers and no look of its own. Give it a radius, a border and a background.
import { SpotlightSurface } from '@youj/motion'
export function Announcement() {
return (
<SpotlightSurface className="rounded-2xl border border-line bg-surface p-5">
<p>Enterprise: SAML SSO, audit logs and a 99.99% SLA.</p>
</SpotlightSurface>
)
}Wrap several surfaces in a SpotlightGroup to share one light between them. The group tracks the pointer once and gives every surface the position in its own coordinates, so a card lights along the edge nearest the cursor even while the cursor is over its neighbor.
import { SpotlightGroup, SpotlightSurface } from '@youj/motion'
<SpotlightGroup className="grid grid-cols-3 gap-3">
{plans.map((plan) => (
<SpotlightSurface key={plan.id} className="rounded-2xl border border-line bg-surface">
<PlanOption plan={plan} />
</SpotlightSurface>
))}
</SpotlightGroup>The group adds no layout, so bring your own grid or flex classes. radius sets the size of the light in pixels, 260 by default, on the group for every surface or on a single surface.
Description
Three plans in one group: the light follows the pointer and spills onto the neighboring cards.
When a control inside a surface receives keyboard focus, the light parks at the center of that surface. Moving focus to the next surface glides the light across instead of jumping it. Tab into the plans in the demo, or use the arrow keys between them.
The light follows a mouse or a pen. Touch doesn't move it, because on a touchscreen there's no pointer to follow between taps.
An Enterprise surface that tracks the pointer on its own.
Outside a group, a surface tracks the pointer itself. The light fades in over the surface under the pointer and glows along its border where it passes.
A dot grid that lights in the accent around the pointer, driven by useSpotlight.
useSpotlight(ref) is the tracker without the look. It writes the light's position as --yj-spot-x and --yj-spot-y, in pixels from the top-left corner, onto the element in ref when it carries data-yj-spot-card, or else onto every [data-yj-spot-card] inside it. Use the variables in your own CSS. Here they move a mask that shows an accent copy of a dot grid.
import { useRef } from 'react'
import { useSpotlight } from '@youj/motion'
function EdgeNetwork() {
const ref = useRef<HTMLDivElement>(null)
useSpotlight(ref)
return (
<div ref={ref} data-yj-spot-card="" className="relative overflow-hidden rounded-2xl border border-line">
<div
aria-hidden
className="absolute inset-0 [mask-image:radial-gradient(150px_circle_at_var(--yj-spot-x,-999px)_var(--yj-spot-y,-999px),black,transparent)] …"
/>
…
</div>
)
}Give the variables a far-off fallback, as above, so nothing is lit before the pointer arrives. The hook also sets data-yj-spot-active on the root while the light is on, and data-yj-spot-mode to pointer or focus. Pass false as its second argument to switch it off.
Code
spotlight-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsradius, children, radius… (4)
Props
SpotlightSurface
| Prop | Type | |
|---|---|---|
radius | numberRadius of the light in px when used outside a SpotlightGroup. default 260 | 260 |
children | ReactNode | – |
Other props pass through: extends HTMLAttributes<HTMLDivElement>. | ||
SpotlightGroup
| Prop | Type | |
|---|---|---|
radius | numberRadius of the light in px. default 260 | 260 |
children | ReactNode | – |
Other props pass through: extends HTMLAttributes<HTMLDivElement>. | ||
useSpotlight
function useSpotlight(root: RefObject<HTMLElement | null>, enabled?: boolean): void| Argument | Type | Default | Description |
|---|---|---|---|
root | RefObject<HTMLElement | null> | The element to track over. Surfaces are root itself when it has data-yj-spot-card, otherwise every [data-yj-spot-card] inside it. | |
enabled | boolean | true | Turns tracking off. It's also off under reduced motion. |
MotionPointer, keyboard glide, glow in and out · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Pointerthe light follows the pointer | no token one write per animation frame | one write per animation frame | |
| Keyboard glidethe light moves to the focused surface | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
Glow in and outsurface glow and border glow, opacity | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
No tracking and no glow. The hovered or focused surface shows a static accent border instead.
The light is a radial gradient moved with transform, and the border glow is the same gradient masked down to a 1px ring. Pointer moves are batched to one style write per frame, and nothing triggers layout.
Reduced motion
Nothing tracks the pointer. The hovered or focused surface shows a static accent border, so a reader still sees which surface they're on, and the light never moves. useSpotlight doesn't write its variables at all, so design your own CSS to stand on its own unlit, as the dot grid in the custom-light preview does.
AccessibilityKeyboard, ARIA, focus
Accessibility
- The lit layers are
aria-hiddenand ignore the pointer. The effect adds no roles and no tab stops, so a surface reads exactly like its content. - Keyboard users get the same light as pointer users: focus parks it on the focused surface. Keep a visible focus ring on the control itself too, as the plans in the demo do with
has-focus-visible:ring-2, because a glow alone isn't a focus indicator. - The light is decoration. Selection, state and meaning belong in the content, like the check on the chosen plan.