Scroll-Driven Animations

Pure CSS — animation-timeline: scroll() for progress, view() for reveal-on-enter. No scroll event listeners.

How it works

The panel below is its own scroll container (overflow: auto). The lime bar tracks scroll progress via animation-timeline: scroll(nearest). The blue side meter uses a named scroll-timeline-name: --article-scroll on the same scroller. Cards animate with view() timelines as they enter, cover, or exit the scrollport.

scroll() Maps scroll offset → animation progress. Perfect for progress bars.
view() Maps element visibility in scrollport → animation progress. Perfect for reveals.

Live demo — scroll inside

entry 0% → entry 100%

Fade + slide up on enter

animation-range: entry 0% entry 100% — runs while the card crosses the scrollport edge. Classic reveal pattern, zero JS.

cover 0% → cover 50%

Scale + slide during cover phase

animation-range: cover 0% cover 50% — animates while the element occupies the scrollport, not just at the boundary.

entry (default)

Another entry reveal

Each card owns its own anonymous view() timeline. No IntersectionObserver, no requestAnimationFrame loop.

cover phase

Compositor-friendly motion

Only transform and opacity here — properties the browser can promote and animate off the main thread.

exit 0% → exit 100%

Subtle fade on exit

animation-range: exit 0% exit 100% — runs as the card leaves the scrollport. Scroll back up to see it restore.

entry

End of scroll area

Progress bar should read 100%. Side meter fills completely. All driven by CSS scroll timelines — not JavaScript.

Scroll inside the panel ↑↓ · Named timeline powers the blue meter

Minimal CSS

/* Named scroll timeline on the scroller */
.scroll-panel {
  scroll-timeline-name: --article-scroll;
}

/* Progress bar — anonymous scroll() on nearest ancestor */
.progress-bar {
  animation: grow linear;
  animation-timeline: scroll(nearest block);
}

/* Side meter — named timeline reference */
.side-meter-fill {
  animation: grow-v linear;
  animation-timeline: --article-scroll;
}

/* Card reveal — view() + animation-range */
.card {
  animation: reveal linear both;
  animation-timeline: view(block);
  animation-range: entry 0% entry 100%;
}