Split reveal
A headline broken into words, each rising on its own beat, SplitText without the engine.
Words arrive one by one
and the copy follows.
<section data-sv data-sv-once class="py-24">
<h2 class="sv-split-rise text-5xl font-extrabold" data-sv-split>
Words arrive one by one
</h2>
</section>
<!-- knobs per element: [--sv-stagger:60ms] [--sv-duration:600ms] -->
<section data-sv data-sv-once>
<h2 class="sv-split-rise" data-sv-split>Words arrive one by one</h2>
</section>
<!-- data-sv-split wraps each word in a span with --sv-order; the full text
stays in a visually-hidden span for AT and the animated spans are
aria-hidden. char mode: data-sv-split="char". -->
/* the preset (styles/core.css): */
.sv, [data-sv] { --sv-live: 0; }
.sv.sv-live { --sv-live: 1; }
/* non-replaced inline boxes ignore translate: the animated spans need it */
.sv-split > span[aria-hidden] { display: inline-block; }
.sv-on .sv .sv-split-rise > span {
opacity: var(--sv-live); translate: 0 calc((1 - var(--sv-live)) * 0.6em);
transition: opacity var(--sv-duration, 800ms) var(--sv-ease, cubic-bezier(0.28, 0.84, 0.42, 1)),
translate var(--sv-duration, 800ms) 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]) .sv-split-rise > span {
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]) .sv-split-rise > span { transition: none; animation: none; opacity: 1; translate: none; }
/* a focused split heading must reveal every word at once, not after its
stagger delay (WCAG 2.4.7): must sit AFTER the reduce blocks above, same
source-order rule */
.sv-on .sv .sv-split-rise:focus-within > span { transition: none; opacity: 1; translate: none; }
/* scrub instead of play: the same spans feed sv-reading directly */
<h2 class="sv-reading" data-sv-split>…</h2> <!-- inside a data-sv-pin -->
import { Split } from 'scrollvars/react'
<Reveal>
<Split as="h2" className="sv-split-rise" stagger={60}>
Words arrive one by one
</Split>
</Reveal>
{/* <Split> renders the spans ON THE SERVER. The split markup is in the
HTML, so no client-side splitting, no layout shift, no hydration
flash. by="char" for letters. */}