Skip to content

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/motion

For 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.

Reference

Propsradius, children, radius… (4)

Props

SpotlightSurface

SpotlightSurface props
PropType
radiusnumber

Radius of the light in px when used outside a SpotlightGroup.

default 260

260
childrenReactNode–
Other props pass through: extends HTMLAttributes<HTMLDivElement>.

SpotlightGroup

SpotlightGroup props
PropType
radiusnumber

Radius of the light in px.

default 260

260
childrenReactNode–
Other props pass through: extends HTMLAttributes<HTMLDivElement>.

useSpotlight

function useSpotlight(root: RefObject<HTMLElement | null>, enabled?: boolean): void
ArgumentTypeDefaultDescription
rootRefObject<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.
enabledbooleantrueTurns tracking off. It's also off under reduced motion.
MotionPointer, keyboard glide, glow in and out · reduced motion

Motion

Motion tokens
MomentTokens
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.standard

340ms · 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.standard

340ms · 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-hidden and 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.