All articles
Animation

CSS Scroll-Driven Animations: No JavaScript Required

4 November 2024 7 min read Animation

Use the new CSS scroll-driven animations API to create scroll-triggered reveals, progress bars and parallax effects entirely in CSS. Includes real examples and browser support table.

Scroll-driven animations have been a JavaScript staple for years — libraries like GSAP, AOS and Intersection Observer all do versions of this. Now CSS can do it natively, with zero JavaScript and better performance.

The animation-timeline property

The new animation-timeline property connects a CSS animation to a scroll position instead of time:

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

.card {
  animation: reveal linear;
  animation-timeline: scroll();
  animation-range: entry 0% entry 100%;
}

scroll() refers to the nearest scrolling ancestor. animation-range defines when in the scroll the animation plays.

Scroll progress indicator

The classic reading progress bar, in pure CSS:

.progress-bar {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  background: var(--accent);
  transform-origin: left;
  animation: progress linear;
  animation-timeline: scroll(root);
}

@keyframes progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
View timeline — trigger on element entering viewport
.reveal-item {
  opacity: 0;
  transform: translateY(40px);
  animation: reveal-up linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 60%;
}

@keyframes reveal-up {
  to { opacity: 1; transform: translateY(0); }
}

view() creates a timeline based on the element's position in the viewport. animation-range: entry 0% entry 60% means the animation plays while the element is entering the viewport — starting at 0% visible, completing at 60% visible.

Browser support — check before shipping

Scroll-driven animations require Chrome 115+ or Edge 115+. Firefox and Safari support is improving but not yet universal. Always add a feature check:

@supports (animation-timeline: scroll()) {
  .card {
    animation: reveal linear;
    animation-timeline: scroll();
    animation-range: entry 0% entry 100%;
  }
}

/* Fallback for unsupported browsers */
.card {
  opacity: 1;
  transform: none;
}