Scroll-Driven Animations - Animation Timeline API

What You'll Learn

  • Understanding scroll-driven animations with animation-timeline
  • Using scroll() to track scroll container position
  • Using view() to track element visibility
  • Creating scroll progress indicators
  • Building fade-in and slide-in effects on scroll
  • Understanding animation-range for viewport control
  • Creating parallax effects with scroll timelines
  • Building practical patterns: progress bars, reveals, staggered animations

Introduction to Scroll-Driven Animations

Scroll-driven animations are a cutting-edge CSS feature that allows animations to be controlled by scroll position—no JavaScript required! Before this feature, creating scroll-based animations required IntersectionObserver API, scroll event listeners, or third-party libraries like GSAP ScrollTrigger.

With the new animation-timeline property, you can create animations that respond to page scroll (scroll()) or element visibility (view()). These animations are GPU-accelerated and run on the compositor thread, making them incredibly performant.

Animation Timeline Basics

The animation-timeline property replaces the default time-based timeline with a scroll-based one. There are two main timeline types:

/* scroll() - tracks scroll container position */ .element { animation: fadeIn linear; animation-timeline: scroll(root); /* Document scroll */ animation-timeline: scroll(nearest); /* Nearest scrollable ancestor */ animation-timeline: scroll(self); /* Element's own scroll */ } /* view() - tracks element visibility in viewport */ .element { animation: fadeIn linear; animation-timeline: view(); /* Default block axis */ animation-timeline: view(block); /* Vertical scroll */ animation-timeline: view(inline); /* Horizontal scroll */ }

Scroll Progress Bar

A scroll progress indicator at the top of the page is one of the most common use cases. It scales from 0 to 100% as the user scrolls through the page.

.progress-bar { position: fixed; top: 0; left: 0; width: 100%; height: 4px; background: #3b82f6; transform-origin: left; animation: scroll-progress linear; animation-timeline: scroll(root); z-index: 1000; } @keyframes scroll-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

Fade In on Scroll

Elements can smoothly fade in as they enter the viewport. The view() timeline tracks when the element becomes visible.

.fade-in-scroll { opacity: 0; animation: fade-in linear forwards; animation-timeline: view(); animation-range: entry 0% cover 30%; } @keyframes fade-in { to { opacity: 1; } }

The animation-range property controls when the animation starts and stops during the element's journey through the viewport. Here, it fades in from when it enters (entry 0%) until it's 30% covered (cover 30%).

Slide In Effects

Combine transforms with opacity for slide-in effects. Elements can slide from any direction as they enter the viewport.

/* Slide in from left */ .slide-in-left { opacity: 0; transform: translateX(-100px); animation: slide-in-left-anim linear forwards; animation-timeline: view(); animation-range: entry 0% cover 40%; } @keyframes slide-in-left-anim { to { opacity: 1; transform: translateX(0); } } /* Slide in from right */ .slide-in-right { opacity: 0; transform: translateX(100px); animation: slide-in-right-anim linear forwards; animation-timeline: view(); animation-range: entry 0% cover 40%; } @keyframes slide-in-right-anim { to { opacity: 1; transform: translateX(0); } }

Animation Range Explained

The animation-range property is crucial for scroll-driven animations. It defines when the animation starts and stops relative to the element's position in the viewport.

/* Named ranges for view() timeline */ animation-range: entry 0% cover 50%; /* entry: element entering viewport cover: element covering/within viewport exit: element leaving viewport contain: viewport fully contains element */ /* Percentage ranges */ animation-range: 0% 100%; /* Full journey */ animation-range: 25% 75%; /* Middle 50% */ /* Common patterns */ animation-range: entry 0% entry 100%; /* While entering */ animation-range: cover 0% cover 100%; /* While in view */ animation-range: exit 0% exit 100%; /* While exiting */ animation-range: entry 0% exit 100%; /* Entire journey */

Scale and Rotate on Scroll

You can animate any transform property based on scroll position. These effects add visual interest to page scrolling.

/* Scale up on scroll */ .scale-up { transform: scale(0.5); opacity: 0; animation: scale-up-anim linear forwards; animation-timeline: view(); animation-range: entry 0% cover 50%; } @keyframes scale-up-anim { to { transform: scale(1); opacity: 1; } } /* Rotate on scroll */ .rotate-scroll { animation: rotate-scroll-anim linear; animation-timeline: view(); animation-range: entry 0% exit 100%; } @keyframes rotate-scroll-anim { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

Parallax Effects

Create parallax effects where background elements move at different speeds than the scroll. This adds depth to your designs.

.parallax-container { height: 400px; overflow: hidden; position: relative; } .parallax-bg { position: absolute; width: 100%; height: 120%; animation: parallax-scroll linear; animation-timeline: view(); animation-range: entry 0% exit 100%; } @keyframes parallax-scroll { from { transform: translateY(-10%); } to { transform: translateY(10%); } }

Staggered Animations

Create sequential animations by using different animation-range values for each element. This creates a cascading reveal effect.

.stagger-item { opacity: 0; transform: translateY(50px); animation: stagger-anim linear forwards; animation-timeline: view(); } .stagger-item:nth-child(1) { animation-range: entry 0% cover 30%; } .stagger-item:nth-child(2) { animation-range: entry 5% cover 35%; } .stagger-item:nth-child(3) { animation-range: entry 10% cover 40%; } .stagger-item:nth-child(4) { animation-range: entry 15% cover 45%; } .stagger-item:nth-child(5) { animation-range: entry 20% cover 50%; } @keyframes stagger-anim { to { opacity: 1; transform: translateY(0); } }

Image Reveal with Clip-Path

Use clip-path animations to create wipe or curtain reveal effects for images as they scroll into view.

.image-reveal { clip-path: inset(0 100% 0 0); animation: reveal-image linear forwards; animation-timeline: view(); animation-range: entry 0% cover 60%; } @keyframes reveal-image { to { clip-path: inset(0 0% 0 0); } }

Section Progress Indicators

Add visual feedback showing how far through a section the user has scrolled. Useful for long articles or documentation.

.section-with-indicator { position: relative; } .section-indicator { position: absolute; left: 0; top: 0; width: 4px; height: 100%; background: #667eea; transform-origin: top; animation: section-progress linear; animation-timeline: view(); animation-range: entry 0% exit 100%; } @keyframes section-progress { from { transform: scaleY(0); } to { transform: scaleY(1); } }

Performance Considerations

Scroll-driven animations are GPU-accelerated and highly performant. However, follow these best practices:

Feature Detection

Since browser support is limited, use feature detection to provide fallbacks or alternative experiences.

/* Only apply scroll animations if supported */ @supports (animation-timeline: view()) { .fade-in { opacity: 0; animation: fade-in linear forwards; animation-timeline: view(); animation-range: entry 0% cover 30%; } } /* Fallback for unsupported browsers */ @supports not (animation-timeline: view()) { .fade-in { opacity: 1; /* Just show the element */ } } // Check support in JavaScript if (CSS.supports('animation-timeline', 'view()')) { console.log('Scroll-driven animations are supported!'); // Apply scroll animation classes } else { console.log('Scroll-driven animations not supported'); // Use IntersectionObserver fallback }

Accessibility Considerations

Always respect user preferences for reduced motion. Scroll animations can be disorienting for some users.

/* Disable scroll animations for users who prefer reduced motion */ @media (prefers-reduced-motion: reduce) { * { animation-timeline: auto !important; animation-duration: 0.01ms !important; } }

Key Takeaways