CSS Scroll-Driven Animations: Zero-JS Parallax Scrolling, Progress Bars, and Reveal Effects

前端工程(Updated Jun 2, 2026)

Farewell to Scroll Event Listeners

Traditional scroll animations rely on JavaScript scroll event listeners:

// ❌ Traditional approach: JS listener + forced synchronous layout
window.addEventListener('scroll', () => {
  const progress = window.scrollY / (document.body.scrollHeight - window.innerHeight);
  progressBar.style.width = `${progress * 100}%`;
  // Every scroll triggers a reflow!
});

CSS scroll-driven animations accomplish all this with pure CSS, running on the compositor thread without blocking the main thread:

/* ✅ Pure CSS: Reading progress bar */
.progress-bar {
  width: 100%;
  height: 3px;
  background: blue;
  transform-origin: left;
  animation: progress linear;
  animation-timeline: scroll();
}

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

scroll-timeline Syntax

Anonymous Scroll Timeline

/* Based on the nearest scrollable ancestor's scroll */
.animated {
  animation-timeline: scroll();
}

/* Specify direction */
.animated-x {
  animation-timeline: scroll(x); /* Horizontal scroll */
}

.animated-y {
  animation-timeline: scroll(y); /* Vertical scroll */
}

Named Scroll Timeline

/* Declare timeline name on the scroll container */
.scroll-container {
  scroll-timeline: --page-scroll y;
  overflow-y: auto;
}

/* Reference on animated elements */
.animated-element {
  animation: fade-in linear;
  animation-timeline: --page-scroll;
}

view-timeline Syntax

view-timeline is based on the progress of an element entering/leaving the viewport:

.reveal-item {
  view-timeline: --item-reveal;
  animation: reveal linear both;
  animation-timeline: --item-reveal;
}

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

view-timeline-range: Precise Animation Range Control

.reveal-item {
  view-timeline: --item-reveal;
  animation: reveal linear both;
  animation-timeline: --item-reveal;
  /* Play animation only during the first 30% of element entering the viewport */
  animation-range: entry 0% entry 30%;
}
range Value Meaning
entry Element begins entering the viewport
exit Element begins leaving the viewport
cover Element is fully within the viewport
contain Element from fully entering to beginning to leave
entry-crossing Element is crossing the viewport edge entering

Example 1: Parallax Scrolling

.parallax-container {
  overflow-y: auto;
  scroll-timeline: --parallax y;
  height: 100vh;
}

.parallax-bg {
  animation: parallax-move linear;
  animation-timeline: --parallax;
}

@keyframes parallax-move {
  from { transform: translateY(0); }
  to { transform: translateY(-200px); }
}

/* Different parallax speeds for different depths */
.parallax-layer-1 { animation: parallax-move-slow linear; animation-timeline: --parallax; }
.parallax-layer-2 { animation: parallax-move-medium linear; animation-timeline: --parallax; }
.parallax-layer-3 { animation: parallax-move-fast linear; animation-timeline: --parallax; }

@keyframes parallax-move-slow { to { transform: translateY(-50px); } }
@keyframes parallax-move-medium { to { transform: translateY(-150px); } }
@keyframes parallax-move-fast { to { transform: translateY(-300px); } }

Example 2: Reading Progress Bar

/* Reading progress indicator at the top of the page */
.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: linear-gradient(90deg, #3b82f6, #8b5cf6);
  transform-origin: left;
  z-index: 1000;
  animation: reading-progress linear;
  animation-timeline: scroll(root y);
}

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

Example 3: Scroll Reveal

/* Fade in from below */
.reveal-up {
  view-timeline: --reveal;
  animation: reveal-up linear both;
  animation-timeline: --reveal;
  animation-range: entry 0% entry 40%;
}

@keyframes reveal-up {
  from {
    opacity: 0;
    transform: translateY(80px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Slide in from the left */
.reveal-left {
  view-timeline: --reveal;
  animation: reveal-left linear both;
  animation-timeline: --reveal;
  animation-range: entry 0% entry 30%;
}

@keyframes reveal-left {
  from {
    opacity: 0;
    transform: translateX(-60px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

Staggered Delay

/* Use animation-delay for staggered effects */
.stagger-item:nth-child(1) { animation-delay: 0ms; }
.stagger-item:nth-child(2) { animation-delay: 80ms; }
.stagger-item:nth-child(3) { animation-delay: 160ms; }
.stagger-item:nth-child(4) { animation-delay: 240ms; }
.stagger-item:nth-child(5) { animation-delay: 320ms; }

Example 4: Horizontal Scroll Area

.horizontal-scroll {
  overflow-x: auto;
  scroll-timeline: --hscroll x;
}

.card {
  animation: card-appear linear both;
  animation-timeline: --hscroll;
  animation-range: entry 0% entry 20%;
}

@keyframes card-appear {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

Example 5: Sticky Header Shrink

.page {
  scroll-timeline: --page y;
  overflow-y: auto;
}

.sticky-header {
  position: sticky;
  top: 0;
  animation: header-shrink linear both;
  animation-timeline: --page;
  animation-range: 0px 100px;
}

@keyframes header-shrink {
  from {
    padding: 24px;
    font-size: 2rem;
  }
  to {
    padding: 12px;
    font-size: 1.25rem;
  }
}

Example 6: Image Sprite Sequence Animation

.sprite-container {
  scroll-timeline: --sprite y;
  overflow-y: auto;
}

.sprite-image {
  animation: sprite-steps steps(24) both;
  animation-timeline: --sprite;
}

@keyframes sprite-steps {
  from { background-position: 0 0; }
  to { background-position: -2400px 0; } /* 24 frames × 100px width */
}

JS vs CSS Scroll Animation Comparison

Dimension JS (scroll event) CSS scroll-timeline
Performance Main thread, potential jank Compositor thread, 60fps
Code size 20-50 lines 5-10 lines
Compatibility All browsers Chrome 115+, Firefox 125+
Flexibility Arbitrary logic Linear/segmented animations
Debugging console.log DevTools Animations panel
Memory Manual listener cleanup needed Auto-managed

Polyfill Strategy

// Feature detection
const supportsScrollTimeline = CSS.supports('animation-timeline', 'scroll()');

if (!supportsScrollTimeline) {
  // Fallback to IntersectionObserver + CSS class toggling
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      entry.target.classList.toggle('in-view', entry.isIntersecting);
    });
  }, { threshold: 0.2 });

  document.querySelectorAll('.reveal-item').forEach(el => {
    observer.observe(el);
  });
}

Browser Compatibility

Browser scroll-timeline view-timeline animation-range
Chrome 115+
Edge 115+
Firefox 125+
Safari 17.4+

Safari is still implementing this feature. It's recommended to use feature detection + IntersectionObserver fallback.


Best Practices

  1. Prefer view-timeline: Most scroll reveal effects are simpler with view-timeline than scroll-timeline
  2. Precise animation-range control: Avoid animations running when elements are not visible
  3. Pair with will-change: Add will-change: transform, opacity to animated elements
  4. Limit simultaneous animated elements: More than 20 simultaneously running scroll animations may impact performance
  5. Feature detection fallback: Use IntersectionObserver fallback when Safari doesn't support it

Summary

CSS scroll-driven animations are one of the most exciting new frontend features in 2026. They bring classic effects like parallax scrolling, reading progress bars, and scroll reveals back from JavaScript to CSS, with significant performance gains (running on the compositor thread, not blocking the main thread). While Safari support is still incomplete, combined with IntersectionObserver fallbacks, you can start using them in production today.

Try these browser-local tools — no sign-up required →

#滚动驱动动画#CSS动画#scroll-timeline#视差滚动#无JS动画