css-scroll-driven-animations

Installation
SKILL.md

CSS Scroll-Driven Animations

When to use this

  • You want a scroll progress bar, fade-in-on-enter, or sticky-header shrink effect and do not want to ship a scroll-linked JS library for it.
  • You are building for Chromium-based browsers primarily (Chrome/Edge 115+) and can accept a graceful degraded-but-functional fallback on Firefox/Safari via @supports.
  • You want an animation whose timeline is literally scroll position (not time) so it can never desync from the user's actual scroll, including fast flicks and scrollbar drags.
  • You need per-element "enters/leaves viewport" triggers (view-timeline) without an IntersectionObserver.
  • Do NOT use this when you need scrubbed pinning (element stays fixed while content scrolls past it) with precise pixel-level control across all browsers today: use sticky-pinning-sequences (GSAP ScrollTrigger) instead, since native CSS has no pin primitive and Safari/Firefox support for scroll-driven animations is still incomplete as of 2024-2025.

Mental model

Normal CSS animations run on a time timeline: 0% to 100% maps to 0ms to duration ms, driven by the OS clock regardless of anything else on the page. animation-timeline replaces that clock with a different progress source. Two timeline types exist:

scroll() creates a timeline from a scroller's own scroll position: 0% is scrolled-to-start, 100% is scrolled-to-end, on whichever axis and whichever scroller you specify (scroll(root block), scroll(nearest inline), or a named scroller). This is for effects tied to the page's overall scroll, like a top progress bar.

view() creates a timeline from a single element's visibility inside its scrollport: 0% is the moment the element's first edge enters the scrollport, 100% is the moment its last edge exits. This is for "this specific card fades in as it scrolls into view" effects, and is the native replacement for IntersectionObserver-driven fade-ins. Because view() timelines are per-element, you can have hundreds of independently-timed elements with no JS overhead, since browsers compute and update these off the main thread as part of compositing.

animation-range further narrows which portion of that 0-100% timeline maps to the animation's own keyframes, using named regions: entry (element entering the scrollport), contain/cover (fully inside), exit (leaving). animation-range: entry 0% cover 40% means "start the keyframes the instant the element begins entering, finish them by the time it's 40% through being fully covered/visible." This lets you fire a fast reveal near the edges and hold steady mid-viewport, without hand-computing pixel offsets.

Because these run through the same engine as any CSS animation, only compositor-safe properties (transform, opacity, filter, clip-path in supporting engines) stay smooth; anything triggering layout will still jank exactly like a time-based animation would.

Installs
2
First Seen
Aug 30, 2026
css-scroll-driven-animations — avnehsbhatia/ultraui