Skip to main content
Ganesh Joshi
Back to Blogs

Scroll-driven animations: CSS and View Timeline

February 15, 20265 min read
Tutorials
CSS or animation code on screen

Scroll-driven animations create engaging experiences where elements animate based on scroll position rather than time. A progress bar that fills as you read, elements that fade in as they enter the viewport, or parallax effects—all without JavaScript. The CSS Scroll-Driven Animations specification makes this native to browsers.

How scroll-driven animations work

Traditional CSS animations run on a timeline measured in seconds. Scroll-driven animations replace time with scroll progress:

Timeline type Progress measured by
Time-based Seconds (default)
Scroll Timeline Scroll position of a scroller
View Timeline Element's visibility in viewport
/* Time-based animation */
.element {
  animation: fadeIn 1s ease-out;
}

/* Scroll-driven animation */
.element {
  animation: fadeIn linear;
  animation-timeline: scroll();
}

Browser support

Browser Support
Chrome 115+ Full
Edge 115+ Full
Firefox 110+ Partial (flags)
Safari In development

Use feature detection:

@supports (animation-timeline: scroll()) {
  .element {
    animation: fadeIn linear;
    animation-timeline: scroll();
  }
}

Scroll Timeline

A Scroll Timeline tracks the scroll position of a container:

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

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: blue;
  transform-origin: left;

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

As the page scrolls, the progress bar grows from 0 to full width.

Scroll Timeline options

.element {
  /* Track scroll of nearest scrollable ancestor */
  animation-timeline: scroll();

  /* Track root scroller (usually document) */
  animation-timeline: scroll(root);

  /* Track specific axis */
  animation-timeline: scroll(root block); /* Vertical */
  animation-timeline: scroll(root inline); /* Horizontal */
}

Named scroll timelines

For more control, name your timeline:

.scroller {
  overflow-y: scroll;
  scroll-timeline-name: --my-scroller;
  scroll-timeline-axis: block;
}

.animated-element {
  animation: slideIn linear;
  animation-timeline: --my-scroller;
}

View Timeline

A View Timeline tracks when an element enters and exits the viewport:

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(50px); }
  to { opacity: 1; transform: translateY(0); }
}

.card {
  animation: fadeIn linear both;
  animation-timeline: view();
}

The card fades in as it scrolls into view.

View Timeline ranges

Control when the animation runs:

.card {
  animation: fadeIn linear both;
  animation-timeline: view();

  /* Start when entering, end when fully visible */
  animation-range: entry 0% cover 100%;
}
Range keyword Meaning
cover From first pixel entering to last pixel leaving
contain From fully inside to starts leaving
entry Element entering viewport
exit Element exiting viewport
entry-crossing Element crossing entry edge
exit-crossing Element crossing exit edge

Percentage offsets

Fine-tune animation timing:

.card {
  animation: fadeIn linear both;
  animation-timeline: view();

  /* Fade in during first 50% of visibility */
  animation-range: entry 0% entry 100%;
}

Common patterns

Reveal on scroll

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

.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

Parallax effect

@keyframes parallax {
  from { transform: translateY(-50px); }
  to { transform: translateY(50px); }
}

.parallax-layer {
  animation: parallax linear;
  animation-timeline: scroll();
}

Horizontal scroll gallery

.gallery {
  overflow-x: scroll;
  scroll-timeline-name: --gallery;
  scroll-timeline-axis: inline;
}

.gallery-item {
  animation: scaleUp linear both;
  animation-timeline: view(inline);
}

@keyframes scaleUp {
  0%, 100% { transform: scale(0.8); opacity: 0.5; }
  50% { transform: scale(1); opacity: 1; }
}

Reading progress indicator

.progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(to right, #3b82f6, #8b5cf6);
  transform-origin: left;
  animation: progress linear;
  animation-timeline: scroll(root);
}

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

JavaScript fallback

For browsers without support, use Intersection Observer:

if (!CSS.supports('animation-timeline', 'scroll()')) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
      }
    });
  }, { threshold: 0.1 });

  document.querySelectorAll('.reveal').forEach((el) => {
    observer.observe(el);
  });
}
/* Fallback styles */
.reveal {
  opacity: 0;
  transform: translateY(100px);
  transition: opacity 0.5s, transform 0.5s;
}

.reveal.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Enhanced styles when supported */
@supports (animation-timeline: scroll()) {
  .reveal {
    opacity: 1;
    transform: none;
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 40%;
  }
}

Accessibility

Respect reduced motion preferences:

@media (prefers-reduced-motion: reduce) {
  .reveal {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .progress {
    animation: none;
    transform: scaleX(1);
  }
}

Performance benefits

Approach Performance
JavaScript scroll handler Main thread, can cause jank
requestAnimationFrame Better, still main thread
CSS scroll-driven Off main thread, compositor

CSS scroll-driven animations run on the compositor thread and don't cause layout recalculations. They're significantly smoother than JavaScript alternatives.

Debugging

Chrome DevTools supports scroll-driven animations:

  1. Open Elements panel
  2. Select animated element
  3. Check Animations panel for timeline visualization
  4. Use Performance panel to verify compositor usage

Summary

CSS scroll-driven animations tie animation progress to scroll position. Use scroll() for scroll containers and view() for viewport visibility. Control timing with animation-range. The API runs off the main thread for better performance than JavaScript. Provide fallbacks with Intersection Observer for unsupported browsers and respect reduced motion preferences.

Frequently Asked Questions

Scroll-driven animations are CSS animations where progress is tied to scroll position instead of time. An element can fade in as you scroll to it or animate along a path as you scroll down the page.

A View Timeline tracks an element's visibility in the viewport. You can animate based on when an element enters, is fully visible, or exits the viewport. It's set with animation-timeline: view().

Chrome 115+ and Edge 115+ have full support. Firefox has partial support. Safari is working on implementation. Use feature detection and provide fallbacks for unsupported browsers.

Use a scroll timeline on the document scroller. Animate a bar's scaleX from 0 to 1 tied to scroll position. The bar visually shows how far down the page the user has scrolled.

CSS scroll-driven animations run off the main thread and don't cause layout thrashing. They're more performant than JavaScript scroll handlers that update styles on every scroll event.

Related Posts