Scroll spotlight
Copy lines that brighten as they cross a reading band, driving a sticky product visual.
API reference
Components
<ScrollSpotlight />
The provider that owns the single active-index state the whole mechanic shares. Renders no DOM of its own - the consumer owns all layout - it only supplies the context every other part and useSpotlight reads.
focusOffset?
Half-height of the reading band as a percentage of the viewport, insetting the top and bottom so the band is a thin slice across the middle. Higher = a tighter band (a line must sit nearer dead-centre to become active). Default 45.
dimmedOpacity?
The resting opacity of a non-active copy line under full motion (the active line always sits at full opacity). Default 0.4.
revealScale?
The scale a sticky/inline visual reveals FROM (it settles to 1), the depth of the crossfade reveal. Dropped under calm (opacity only). Default 1.03.
initialItem?
The item index active before any line has crossed the band. Default 0.
initialLine?
The SpotlightCopy lineId treated as active before any scroll, so the opening line reads at full opacity on mount. Pass your first line's id (e.g. "0-t"). Default "" (no line pre-lit).
children?
The walkthrough: your copy column, rail, stage and visuals.
<SpotlightCopy />
A single line of walkthrough copy that dims to the resting opacity until it crosses the reading band, then brightens - and, on crossing, reports itself as the active line/index so the rail and sticky visual advance with it. Compositor-only (opacity), timed by the theme's snap spring. Under reduced motion it stays fully legible with no scroll tracking.
tag
The element to render. Headings (h2/h3/h4) carry the walkthrough structure; p is a body line.
lineId
A stable id for THIS line, unique across the walkthrough (e.g. ${item}-t for a title, ${item}-b${n} for body lines). It is what marks the line active, not the DOM id.
itemIndex
The item (feature) index this line belongs to. When the line crosses the band it becomes the active index, advancing the rail and the sticky visual.
id?
The element's DOM id, for aria-labelledby wiring (a heading labelling its article). The dimming is purely visual and never hidden from a screen reader.
className?
Merged onto the element - style the copy here.
children?
The line's text.
<SpotlightRail />
The discrete step-dot column for the walkthrough. Discrete on purpose: the steps are distinct features, not a continuous quantity, so a filled gauge would imply data that is not there. The active dot scales up on the theme's snap spring and carries a static primary halo; reached dots fill primary, unreached use a faint tone, and the colour shift is a CSS transition on the snap token. Decorative (the copy carries the meaning) - the consumer's column that wraps it is aria-hidden.
count
How many step dots to render (one per walkthrough item).
className?
Merged onto the <ol> - adjust spacing/alignment here.
<SpotlightStage />
The fixed 4:3 stage every product visual sits in, so a sticky crossfade and an inline reveal share exact dimensions. Sticky visuals stack absolutely inside it; an inline visual fills it.
children?
The stacked SpotlightVisuals (sticky column) or a single one (inline).
className?
Merged onto the stage - override the aspect ratio or sizing here.
<SpotlightVisual />
Wraps ONE of the consumer's product visuals with the sticky crossfade + scale reveal. In the sticky column it crossfades in as its index becomes active; inline it reveals once on scroll into view; under reduced motion it renders its final state (opacity fade only under calm, nothing under off). Hands the visual a revealed flag via render-function children so a bar fill or sparkline draw lands with the panel. Reveal (opacity + scale) is timed by the theme's ui/gentle tokens.
index
This visual's item index, matched against the active index to decide whether the sticky panel is foregrounded.
sticky?
Sticky column (true): the panel is absolutely stacked and crossfades in as it becomes the active index. Inline (false, the default): the panel reveals once when it scrolls into view - the layout a consumer uses on mobile and under reduced motion, where there is no sticky column.
className?
Merged onto the animating panel - style the card surface (border, fill, padding) here; the component owns only positioning + the reveal motion.
children?
The product visual. Pass a render function ({ revealed, active }) => … to thread the reveal flag into inner marks, or a plain node when the visual has nothing to sync.
Hooks
useSpotlight
Read the active walkthrough index and the resolved motion mode from the nearest ScrollSpotlight. Use it to render a live position readout (which feature is in focus) and to choose the sticky-vs-inline layout.
useSpotlight(): SpotlightStateReturns
activeItem
The active item index (0-based) - the feature the reading band last entered. Drives your position readout and which sticky visual is shown.
activeLine
The active line id (the lineId of the last SpotlightCopy to cross the band). Finer-grained than activeItem; usually you want activeItem.
fullMotion
True when full motion runs (no prefers-reduced-motion): the sticky column and scroll tracking are live. False under calm/off, when the consumer should stack the visuals inline instead.
calm
True under prefers-reduced-motion with the theme's calm strategy: keep opacity fades, drop travel.
still
True under prefers-reduced-motion with the theme's off strategy: render the final state with no animation at all.