Lexicon / Scroll-driven
What scrolling plays · 13 named effects, each running below
How far you have scrolled is how far the animation has played. The hairline across the top of this page is one.
also called scroll-linked animation · page progress
The animation's progress is the scroll container's progress. Zero JavaScript, and it runs off the main thread — so it cannot jank even while the main thread is busy.
animation-timeline: scroll(root);Every row times its own animation by its own trip across the screen, so no two are at the same point at the same moment.
also called view timeline · element-in-viewport progress
Each element measures its own passage through the viewport, so several different values exist at once. animation-range is the part most people miss — it decides which slice of that passage the animation occupies.
view-timeline-name: --plate;
animation-timeline: --plate;
animation-range: entry 0% cover 45%;The picture holds still while the page keeps moving, and your scrolling becomes the playhead. Scroll back up and it plays backwards.
also called sticky section · pinned stage · ScrollTrigger pin + scrub
Pin holds the stage still while the page keeps scrolling. Scrub is the separate idea that progress follows scroll position rather than elapsed time, which is why it reverses exactly. Most “cinematic” sites are these two words.
position: sticky; top: 0;
/* GSAP */ ScrollTrigger({ pin: true, scrub: 1 })The layers move at different rates, and the difference between the rates is the part you read as distance.
also called differential scroll · multiplane
The oldest trick here and the easiest to overdo. It reads as depth only when the rates differ by a little; large offsets read as broken layout. Built on a view progress timeline it costs no JavaScript.
@keyframes drift { from { transform: translateY(-10%) } to { transform: translateY(10%) } }
animation: drift linear both; animation-timeline: view();
/* each layer gets its own distance; the difference is the depth */You scroll down; the content travels sideways. A list you ranked becomes a row you compare across.
also called pinned horizontal scroll · sideways gallery
Changing the axis changes what the content means: a stack implies rank, a track implies comparison. Travel must be measured from real content — a track that barely overflows reads as a glitch, not a journey.
/* --travel is the track's overflow, measured from its content */
transform: translateX(calc(-1 * var(--travel)));Cards catch at the top one after another and pile over each other as you keep going.
also called card stack · sticky stack
One property on a list. Each item sticks at the same offset so later items slide over earlier ones and the stack builds itself. No library, no measurement, no scroll listener.
li { position: sticky; top: 76px; }It reads how fast you are scrolling rather than where you have got to. Throw the page and the picture leans, then settles.
also called skew on scroll · velocity distortion · momentum skew
Velocity is a genuinely separate signal from position. The asymmetry is the craft: it rises about six times faster than it falls, which is what makes it read as momentum rather than twitch.
v += (target - v) * (target > v ? 0.34 : 0.055);
transform: skewY(calc(var(--vel) * -4deg));velocity 0.00
A figure far down the panel drives the bar at the top of it. One element's passage moves something it sits nowhere near.
also called timeline-scope · remote timeline · cross-tree driving
A scroll timeline is normally visible only to the element's own descendants. timeline-scope lifts it onto a shared ancestor, so a bar at the top can be driven by a figure far below it — something that previously required JavaScript by definition.
timeline-scope: --plate;
/* on a child */ view-timeline-name: --plate;
/* on a sibling */ animation-timeline: --plate;The line draws itself as you arrive, as though someone were tracing it in front of you.
also called stroke-dashoffset scrub · line draw · SVG path reveal
The robustness trick is pathLength="1": it normalises any path to a length of one, so the same two lines of CSS draw any shape without measuring it in JavaScript first.
path { pathLength: 1; stroke-dasharray: 1; stroke-dashoffset: 1 }
animation-timeline: view();
animation-range: entry 5% cover 50%;Draws as you arrive
The sentence lights up a word at a time, and how much of it has arrived is how far you have come down the page.
also called read-along highlight · per-word reveal · text scrub
Each word owns a different slice of one timeline, so the sentence resolves left to right as you descend. The overlap is the entire craft: too little and it strobes word by word, too much and the sentence arrives all at once.
/* per word, i = its index */
animation-range: cover calc(var(--i) * 1.5%)
cover calc(var(--i) * 1.5% + 26%);The next panel covers the one before it rather than pushing it out of the way.
also called panel slide-over · curtain reveal · section overlap
Two rules and nothing else. The outgoing panel stays pinned while the incoming one rides over it, so the change reads as depth rather than travel. Note what is absent: no animation, no timeline, no measurement.
.a { position: sticky; top: 0 }
.b { position: relative; z-index: 2 }Only the band across the middle of the screen is sharp. Everything above and below it falls out of focus.
also called focus falloff · viewport-centre focus · scroll blur
A camera metaphor applied to a list: attention is a narrow band, and everything outside it recedes. Keep the blur under about 6px — past that it stops reading as focus and starts reading as a rendering fault.
@keyframes focusband {
0%, 100% { filter: blur(4.5px); opacity: .34 }
46%, 54% { filter: blur(0); opacity: 1 }
}The figures wait until they are on screen, then run up to their value — once, not every time you pass.
also called odometer · number roll · stat counter
The tabular figures matter more than the easing — proportional numerals change width as they count and the whole row twitches. It fires once: a counter that replays on every pass is a tell that nobody checked it twice.
font-variant-numeric: tabular-nums;
new IntersectionObserver(fn, { threshold: 0.6 })