← /lab

// live demo

Scroll-Driven

Parallax, reveals, and scrub progress with zero JavaScript driving the animation. The browser is the engine: animation-timeline: scroll() ties the progress bar and parallax layers to the page, animation-timeline: view() with animation-range reveals each panel as it enters, and @property interpolates the custom props that morph the pinned scene. Scroll is the only input.

Heads up: this browser doesn’t support CSS Scroll-Driven Animations yet. Everything below is laid out and fully readable — it just won’t move. Try a recent Chrome, Edge, or Firefox to see scroll drive the whole page.

// view() timeline

Each panel animates off its own position in the viewport.

No IntersectionObserver. No scroll listener. Every card below carries its own view() timeline and an animation-range like entry 0% cover 40% — the platform reveals it as it crosses the fold and freezes it there.

01

Slide & fade

Translate + opacity keyed to entry. It rises into place as it enters.

02

Scale up

A @property-registered scale interpolates from 0.82 to 1 across the entry range.

03

Skew in

Rotate + skew unwinding to flat. Pure declarative keyframes, no easing library.

04

Stagger

Same effect, later in the grid — so it triggers later, naturally staggered by layout.

05

Cover range

Reveals over cover instead of entry — tied to time on screen.

06

Exit too

This one also dims toward exit 100% — scroll past and it settles back.

// scroll() scrub

Pinned. Scrubbed by scroll alone.

This panel sticks while you scroll a tall track. A single scroll(root block) timeline drives a registered --p custom property from 0 to 1, and that one value morphs the shape’s rotation, scale, border-radius, and hue. Rewind by scrolling up — it’s the timeline, not a play-once.

// why it matters

Off the main thread, no jank, no library.

Scroll-driven animations run in the browser’s compositor, not in a requestAnimationFrame loop you have to budget. That means no scroll-listener layout thrash, no GSAP, no ScrollTrigger — and it degrades to a clean static layout where the API isn’t present.