ScrollVars / fx
docs · demo · bench · llms.txt · GitHub

Staggered reveal

Children rise in sequence when the section enters the viewport.

Use it for: Heroes, editorial sections, any "content arrives" moment.
Knobs: --sv-distance (travel), --sv-stagger (delay step), --sv-duration, --sv-ease

The headline

Rises second.

Rises third.

Install: npx scrollvars add staggered-reveal
Styles: import 'scrollvars/styles/core.css'

<section data-sv data-sv-once class="py-24">
  <h2 class="sv-rise text-4xl font-bold">Title</h2>
  <p class="sv-rise" data-sv-order="1">Copy</p>
  <p class="sv-rise" data-sv-order="2" data-sv-distance="3rem">More</p>
</section>
<!-- once: import 'scrollvars/styles/core.css' + <ScrollVarsBoot /> in the layout -->
<!-- data-sv-order/-distance/-from/-to become the CSS vars on mount. No
     style attr, works with mapped CMS content, adds zero global CSS.
     Also fine: [--sv-order:1] arbitrary classes for static one-offs;
     sv-stagger on the parent for sequential lists (no attrs at all). -->
<section data-sv>            <!-- tracked; sets --sv-live in the band -->
  <h2 class="sv-rise">Title</h2>
  <p class="sv-rise" style="--sv-order: 1">Copy</p>
</section>

/* needs styles/core.css (or paste the preset): */
.sv, [data-sv] { --sv-live: 0; }
.sv.sv-live { --sv-live: 1; }
.sv-on .sv .sv-rise { opacity: var(--sv-live);
  translate: 0 calc((1 - var(--sv-live)) * var(--sv-distance, 6rem));
  transition: opacity .8s var(--sv-ease, cubic-bezier(0.28, 0.84, 0.42, 1)),
              translate .8s var(--sv-ease, cubic-bezier(0.28, 0.84, 0.42, 1));
  transition-delay: calc(var(--sv-order, 0) * var(--sv-stagger, 90ms)); }

/* the same sheet's reduced-motion override, and it has to sit AFTER the rule
   it beats: equal specificity, later wins */
@media (prefers-reduced-motion: reduce) {
  .sv-on :is(.sv, [data-sv]) :is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r, .sv-drift) {
    transition: none; animation: none; opacity: 1; translate: none; }
}
/* the same under html[data-sv-motion="reduce"], the site's own switch */
.sv-on:where([data-sv-motion="reduce"]) :is(.sv, [data-sv]) :is(.sv-rise, .sv-fade, .sv-slide-l, .sv-slide-r, .sv-drift) { transition: none; animation: none; opacity: 1; translate: none; }

/* a focused entrance child must be visible now, not after its stagger
   delay (WCAG 2.4.7): must sit AFTER the reduce blocks above, same
   source-order rule */
.sv-on .sv .sv-rise:focus-within { transition: none; opacity: 1; translate: none; }
<Reveal auto>            {/* every direct child, stagger for free */}
  <h2>Title</h2>
  <p>Copy</p>
</Reveal>

// or per-child knobs as props:
<Reveal stagger={140}>
  <Item order={0}>first</Item>
  <Item order={1} effect="slide-l" distance="4rem">second</Item>
</Reveal>

Engine: npm i scrollvars, 12.9 KB gzip as ESM (this page's fx/sv.js IIFE: 13.1 KB). All effects respect prefers-reduced-motion and render complete without JS.