Scroll-Driven Animation
CSS animations tied to scroll position rather than time, using the 2023 Scroll-Driven Animations API with animation-timeline: scroll() or view().
Plain English
Traditional animations run on a clock — they start, play for 300ms, and end. Scroll-driven animations run on the scroll bar: as you scroll down, the animation advances; scroll back up, and it reverses. This means a progress bar that fills as you read, a hero image that fades in as it enters the viewport, or a sticky header that shrinks as you scroll past — all in pure CSS with zero JavaScript. The two timeline types serve different purposes: scroll() tracks how far the user has scrolled in a container (0% = top, 100% = bottom), while view() tracks when an element enters and exits the viewport. Because the animation is driven by scroll position rather than time, it naturally plays forward and backward as the user scrolls in either direction.
Technical
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .card { animation: fadeIn linear; animation-timeline: view(); animation-range: entry 0% entry 30%; }. For scroll progress: animation-timeline: scroll(root block) ties to page scroll. animation-range controls when in the scroll timeline the animation plays (e.g., entry = element entering viewport, exit = leaving). Browser support: Chrome 115+, Edge 115+, Firefox 110+ (with flag), Safari in progress as of 2025. For fallback: detect via @supports (animation-timeline: scroll()) and use IntersectionObserver for unsupported browsers. Performance: scroll-driven animations run off the main thread in supporting browsers — they are compositor-based and do not block JS.
Live Demo
Scroll-Driven Animation
Drag the slider to simulate scroll progress and see which elements would animate at each position.
CSS snippet
@keyframes fadeSlideUp {
from { opacity: 0; translate: 0 40px; }
to { opacity: 1; translate: 0 0; }
}
.scroll-reveal {
animation: fadeSlideUp linear both;
animation-timeline: scroll();
animation-range: entry 0% entry 50%;
}scroll-driven animations are tied to scroll position — no JS event listeners needed.
Usage
✓ Good usage
A reading progress bar at the top of a long article that fills from 0% to 100% as the user scrolls to the bottom, implemented in 4 lines of CSS with no JavaScript.
✗ Bad usage
Pinning an entire section and rotating a 3D globe as the user scrolls through it — complex 3D transforms tied to scroll are better handled by GSAP ScrollTrigger on WebGL canvases, not CSS scroll-driven animations.
Recommended values
- Viewport entry reveal: animation-range: entry 0% entry 40%
- Scroll progress indicator: animation-timeline: scroll(root)
- Parallax offset: animation-range: cover 0% cover 100%
- Header shrink: animation-range: 0px 80px on the root scroll
- Stagger via animation-delay + view() per child element
- Always @supports guard with JS IntersectionObserver fallback
Common mistakes
- No @supports guard — the animation-timeline property is silently ignored in Safari/Firefox without the feature check, leaving elements stuck at opacity: 0 forever.
- Animating layout-triggering properties (width, height, top) via scroll — these are not composited and will cause main-thread jank on every scroll event.
- Using scroll-driven animations for interactive controls — if the animation conveys information the user must act on, keyboard-only users who cannot scroll cannot trigger it.
AI Prompt Example
Copy this into Claude, Cursor, Bolt, or v0.
Add scroll-driven reveal animations to the features section cards using the CSS Scroll-Driven Animations API. Each card should animate from opacity: 0 and translateY: 32px to fully visible as it enters the viewport. Use animation-timeline: view() with animation-range: entry 0% entry 50%. Wrap in @supports (animation-timeline: scroll()) and add an IntersectionObserver fallback that adds a .visible class for unsupported browsers. Respect prefers-reduced-motion by setting animation-duration: 0.01ms in the reduced-motion media query.