🚀AdvancedAdvanced Animations

CSS Scroll-Driven Animations

Link CSS animations to scroll position using animation-timeline, creating progress bars, reveal effects, and parallax purely in CSS.

Overview

Scroll-driven animations let you tie a CSS animation's progress to the scroll position, with zero JavaScript. Two timeline types exist: **1. Scroll Progress Timeline** tracks how far a scroll container has scrolled: ```css .progress-bar { animation: grow-bar linear; animation-timeline: scroll(); animation-fill-mode: both; } @keyframes grow-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } } ``` **2. View Progress Timeline** tracks when an element enters/exits the viewport: ```css .card { animation: fade-up linear both; animation-timeline: view(); animation-range: entry 0% entry 40%; } @keyframes fade-up { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } } ``` **Timeline functions:** - `scroll()` scroll progress of the nearest scrollable ancestor - `scroll(root)` scroll progress of the root scrollable element - `scroll(self)` element itself scrolling - `view()` element entering/leaving the viewport **animation-range** controls when in the timeline the animation plays: - `entry 0% entry 100%` while element enters viewport - `exit 0% exit 100%` while element exits viewport - `cover 0% cover 100%` entire time element intersects scrollport

Key Points

  • •animation-timeline: scroll() links animation progress to the nearest scroll ancestor
  • •animation-timeline: view() links animation to the element entering/leaving the scroll viewport
  • •No JavaScript needed; the browser drives timing from scroll position
  • •animation-range lets you target entry, exit, contain, and cover phases of a view timeline
  • •@scroll-timeline is the older API; prefer the newer scroll() and view() function syntax
  • •Named scroll timelines can be created with scroll-timeline-name and referenced by other elements
  • •Combine with animation-fill-mode: both to hold the end state
  • •Supported in Chrome 115+ and Safari 18+

Examples

Reading progress bar at the top of the page

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: #6366f1;
  transform-origin: left;

  animation: progress linear both;
  animation-timeline: scroll(root);
}

@keyframes progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

scroll(root) uses the root document scroll container. The bar grows from 0 to full width as the user scrolls from top to bottom.

Fade and slide in as elements enter the viewport

.reveal {
  opacity: 0;
  transform: translateY(40px);

  animation: reveal-item linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 60%;
}

@keyframes reveal-item {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

The animation plays from 0% to 100% of the element entering the viewport. Once it has entered 60% of the way, the animation is complete and held (fill-mode: both).

Sticky header shrink on scroll

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: white;

  animation: shrink-header linear both;
  animation-timeline: scroll(root);
  animation-range: 0px 200px; /* triggers in first 200px of scroll */
}

@keyframes shrink-header {
  from {
    padding: 24px 32px;
    box-shadow: none;
  }
  to {
    padding: 12px 32px;
    box-shadow: 0 2px 12px rgba(0,0,0,0.08);
  }
}

Named scroll timeline for sibling elements

/* Define a named timeline on the scroll container */
.scroll-section {
  overflow-y: scroll;
  height: 400px;
  scroll-timeline-name: --section-scroll;
  scroll-timeline-axis: block;
}

/* Reference the named timeline from a sibling */
.sibling-indicator {
  animation: track-scroll linear both;
  animation-timeline: --section-scroll;
}

@keyframes track-scroll {
  from { background: #e0e7ff; }
  to   { background: #4338ca; }
}

Named timelines starting with -- let elements outside the scroll ancestor animate based on a specific container's scroll.

Common Mistakes

  • Forgetting animation-fill-mode: both causing the element to snap back to its starting state after the animation
  • Using animation-duration: auto is required when using animation-timeline (you cannot combine a time-based duration with a scroll timeline)
  • Expecting scroll-driven animations to work in Firefox (Chrome 115+ and Safari 18+ only as of 2024)
  • Not testing with prefers-reduced-motion: reduce, scroll animations can cause discomfort

Note

Always wrap scroll-driven animations in a @media (prefers-reduced-motion: no-preference) block. Users who prefer reduced motion should not see scroll-driven parallax or entrance animations.

Exercises

Write CSS to style the HTML scaffold. The preview updates live — when it looks right, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 3

What does animation-timeline: scroll() do?