# scrollvars — llms.txt (guide for AI coding agents)
> Tiny scroll/pointer/click/canvas animation engine: one rAF in, CSS
> variables out. Zero dependencies. Live demo with view-source docs:
> https://scrollvars.dev · Benchmarks: https://scrollvars.dev/bench/
You are working with `scrollvars`, a tiny scroll/pointer/canvas animation
engine. Read this before writing any animation code in a project that uses it.
**Do not add GSAP, Framer Motion, or IntersectionObserver boilerplate for
things this lib already covers.** The boundary: input-driven animation
(scroll/pointer/gesture) is scrollvars' job; time-driven animation
(orchestrated timelines, interruptible springs, layout/exit transitions,
SVG morph) legitimately belongs to GSAP/Framer — a one-shot load intro is
plain CSS keyframes. Mixing for a rare case is fine; that page just loses
the bundle argument.
## Mental model (the one rule)
One global driver reads the scroll position in a single rAF (batched reads,
then batched writes) and outputs **CSS custom properties** on tracked
elements. All motion is then plain CSS reading those variables. JavaScript
never animates; React never re-renders on scroll. If you find yourself putting
scroll values into React state, you are doing it wrong.
## The variables (the entire API surface)
| var | range | meaning |
|---|---|---|
| `--sv-view` | −1 → 0 → 1 | entering from below → centered → leaving above |
| `--sv-t` | 0 → 1 | travel through the viewport (same semantics as native `view()`) |
| `--sv-pin` | 0 → 1 | progress across a pinned (sticky) stretch — curtains, rails, scrubbing |
| `--sv-scene` | 0 → n−1 | scene index of a pinned section, eased and snapped |
| `--mx` / `--my` | −1 → 1 | pointer offset from the element's center (pointer module) |
| `.sv-live` | class | on while inside the activation band (enter 75%, exit 25% of viewport) |
Guard: the driver sets `sv-on` on ``. Entrance CSS must hide content
only under `.sv-on` — without JS everything stays visible (never fail hidden).
The shipped `styles.css` already does this; follow the same pattern for
custom presets.
## Imports
```ts
import { track, trackPointer, scrollToScene, scan, slider } from 'scrollvars' // vanilla core
import { Track, Reveal, Parallax, Scenes, Item, ScrollVarsBoot, useTrack,
useScenes, usePointer, useCanvasEffect, useSlider } from 'scrollvars/react' // React ('use client')
import { mountEffect } from 'scrollvars/canvas' // ambient canvas harness
import 'scrollvars/styles.css' // all presets — or modular:
import 'scrollvars/styles/core.css' // entrances only (1.8 KB gz);
// also styles/pin.css, styles/slider.css, styles/tilt.css — import per page needs
```
## Recipes
**Entrance reveal (most common):** wrap the section, mark children.
```tsx
{/* every direct child rises with a stagger */}
Title
Body
// or per-child control, all knobs as attributes (they compile to the vars):
- first
- second
// VarProps (order, distance, stagger, duration, ease) work on Track/Reveal/
// Parallax/Item — prefer them over style={{'--sv-…'}} in React code.
```
**Tailwind + the vars:** `[--sv-order:1]` is fine for static one-off markup
(each unique value adds one tiny global rule). For mapped/dynamic content use
`style={{'--sv-order': i}}` — required, not just cleaner: Tailwind's JIT scans
source statically and never generates interpolated arbitrary classes. For
sequential children skip the bookkeeping entirely: `sv-stagger` on the parent
orders them via nth-child.
**Parallax drift:** `…` — continuous,
tied to `--sv-t`, no transition (transitions on continuous values rubber-band).
**Pinned scenes (storytelling / horizontal rail / curtain):**
```tsx
{({ scene, goTo }) => }
```
The container is N viewports tall, content is `position: sticky`. For pure-CSS
pinned effects use the presets: `sv-curtain-l/r` (two halves open),
`sv-rail` (horizontal carousel — enters from offscreen right and still moves
when the track fits the viewport:
`translate: calc((1 - var(--sv-pin)) * 100vw + var(--sv-pin) * min(100vw - 100%, 0px)) 0`).
**Zero-wrapper mode (prefer this in Next.js):** one `` in the
root layout, then plain RSC sections with `data-sv` attributes (`data-sv-once`,
`data-sv-pin`, `data-sv-travel`, `data-sv-scenes="4"`, `data-sv-enter="0.6"`/`data-sv-exit="0.2"` (custom live band)) — no client components
in pages at all. Route-change nodes are auto-tracked via MutationObserver.
**Spread (deck → grid):** `sv-spread` — children sit in their real flex row,
a translate collapses them onto the center while `--sv-spread` is 0. Add
`.sv-spread-in` to play on arrival (sv-live + stagger), or map the var to
scrub: `.mine > * { --sv-spread: clamp(0, calc(var(--sv-t) * 2), 1) }`.
Set `--sv-order` per child and `--sv-mid` = (N−1)/2 on the container.
**Sequenced scrub (choreography — do NOT add GSAP for this):** `sv-range` —
each child gets `--sv-r` (0..1) over its own slice of the pin: set
`--sv-from`/`--sv-to` per child, add `sv-range-rise` for the ready-made
flavor or consume `--sv-r` yourself (ALWAYS as `var(--sv-r, 1)` — the calc
division needs Chrome 112/Safari 16.4/FF 112 and the fallback settles old
engines at the end state). JS twin: `mapRange(t, from, to, ease?)` inside
`onPin`/`onTravel` for canvas/WebGL.
**Pinned presets:** `sv-deck` (card pile, children stack via grid, set
`--sv-count`), `sv-reading` (word spans lit across the pin: `--sv-count` on
the container, `--sv-order` per span), `sv-counter` (scroll-driven integer via
`@property` + `counter()`, set `--sv-max`; number renders as `::after`).
**Carousel / slider (do NOT add Swiper):** in React prefer the kit:
``
with `` for per-slide overrides — breakpoints are media
queries (map keys = Tailwind names or raw min-widths). Chrome customization:
var knobs (--sv-arrow-*/--sv-dot-*) globally or per instance → stable
classes (sv-arrow, sv-dot) → prevIcon/nextIcon/renderDot → external UI via
the ref (full SliderHandle). Also `