← Back to DevBytes

Implementing CSS Scroll Timeline in Modern Web Applications

Introduction to CSS Scroll Timeline

Modern web experiences demand more than static layouts. Users expect interfaces that respond fluidly to their interactions, and scrolling is one of the most fundamental gestures on the web. For years, developers relied on JavaScript and libraries like Intersection Observer or GSAP to create scroll-driven animations. Now, CSS Scroll Timeline — part of the scroll-driven animations specification — brings this capability natively to the browser, allowing you to tie animation progress directly to scroll position without writing a single line of JavaScript.

What Is CSS Scroll Timeline?

CSS Scroll Timeline is a new animation timeline type that replaces the traditional time-based timeline with one driven by scroll progression. Instead of an animation playing over seconds or milliseconds, it advances as the user scrolls through a scroll container. The specification defines two primary mechanisms:

Both are specified via the animation-timeline CSS property, which accepts scroll(), view(), or a named timeline defined with the scroll-timeline property.

Why Scroll-Driven Animations Matter

The benefits of native scroll-driven animations go far beyond convenience:

Browser Support and Feature Detection

As of early 2025, scroll-driven animations are supported in Chrome 115+, Edge 115+, and Opera 101+. Firefox has the feature implemented behind a flag and is working toward full support. Safari has expressed positive signals and is actively working on implementation. You should always use feature detection:

/* Feature detection with @supports */
@supports (animation-timeline: scroll()) {
  /* Your scroll-driven animation styles */
  .animated-element {
    animation: reveal linear;
    animation-timeline: view();
  }
}

/* Fallback for unsupported browsers */
@supports not (animation-timeline: scroll()) {
  .animated-element {
    opacity: 1; /* Immediately visible without animation */
  }
}

For JavaScript-based detection, check the property on a computed style:

function supportsScrollTimeline() {
  const el = document.createElement('div');
  el.style.animationTimeline = 'scroll()';
  return el.style.animationTimeline !== '';
}

if (supportsScrollTimeline()) {
  // Enable scroll-driven features
}

Core Concepts: The Anatomy of a Scroll Timeline

To use scroll(), you need three essential pieces working together:

The scroll() function accepts two optional arguments:

/* Syntax */
animation-timeline: scroll([<scroller>] [<axis>]);

/* <scroller> values: nearest | root | self */
/* <axis> values: block | inline | vertical | horizontal */

The axis parameter defaults to block, which maps to vertical in horizontal writing modes and horizontal in vertical writing modes.

Anonymous vs. Named Scroll Timelines

When you use scroll() directly inside animation-timeline, you're creating an anonymous timeline. For sharing a single timeline across multiple animations, define a named timeline with the scroll-timeline shorthand:

/* Define a named timeline on the scroll container */
.scroll-container {
  overflow-y: scroll;
  scroll-timeline: --gallery-scroller block;
  /* Shorthand: scroll-timeline-name + scroll-timeline-axis */
}

/* Multiple elements can reference the same timeline */
.card:nth-child(1) {
  animation: fadeIn linear;
  animation-timeline: --gallery-scroller;
}

.card:nth-child(2) {
  animation: slideUp linear;
  animation-timeline: --gallery-scroller;
}

Named timelines are powerful when you have a shared scroll container and want coordinated animations across multiple elements, all driven by the same scroll progress.

View Progress Timeline with view()

The view() function creates a timeline based on an element's visibility within a scrollport. It's ideal for effects where elements animate as they scroll into view. The timeline starts when the element begins to enter the scrollport and ends when it fully exits.

/* Syntax */
animation-timeline: view([<axis>] [<view-inset>]);

/* Examples */
animation-timeline: view();                          /* default: block axis, auto insets */
animation-timeline: view(block 10px);                /* 10px inset on start and end */
animation-timeline: view(block 20% 40%);             /* different start and end insets */
animation-timeline: view(inline);                    /* horizontal axis */

The view-inset values adjust when the timeline begins and ends. A positive inset starts the animation later (the element must be further inside the viewport). A negative inset starts earlier (animation begins before the element fully enters). This gives you fine-grained control over animation timing:

/* Animation starts when the element's top is 100px inside the viewport
   and ends when its bottom is 50px from the bottom edge */
.element {
  animation: reveal linear;
  animation-timeline: view(block 100px 50px);
}

View timelines also support timeline ranges via the animation-range property, which lets you target specific phases like entry, exit, contain, and cover:

/* Animate only during the entry phase */
.element {
  animation: fadeIn linear;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

/* Animate during the exit phase */
.element {
  animation: fadeOut linear;
  animation-timeline: view();
  animation-range: exit 0% exit 100%;
}

Practical Example: Scroll-Progress Bar

One of the most common use cases is a reading progress indicator. Here's a complete implementation:

<!-- HTML -->
<header class="reading-header">
  <div class="progress-bar"></div>
</header>
<main class="article-content">
  <!-- Long article content here -->
</main>
/* CSS */
.article-content {
  /* This is the scroll container */
  overflow-y: auto;
  height: 100vh;
  /* Define a named timeline for sharing */
  scroll-timeline: --article-progress vertical;
}

.progress-bar {
  transform-origin: left center;
  /* Scale from 0 to 100% as the user scrolls */
  animation: growBar linear;
  animation-timeline: --article-progress;
  height: 4px;
  background: linear-gradient(to right, #6366f1, #a855f7);
  width: 100%;
}

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

The progress bar scales horizontally from 0% to 100% as the user scrolls through the article. No JavaScript, no event listeners — just declarative CSS.

Practical Example: Reveal-on-Scroll Cards

Using view(), you can create elegant entrance animations for a grid of cards:

<div class="card-grid">
  <div class="card">Card 1</div>
  <div class="card">Card 2</div>
  <div class="card">Card 3</div>
  <div class="card">Card 4</div>
  <div class="card">Card 5</div>
  <div class="card">Card 6</div>
</div>
/* CSS */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  padding: 40px;
  /* The body or a wrapper is the scroll container */
}

.card {
  padding: 40px;
  background: #1e293b;
  border-radius: 12px;
  color: #f8fafc;
  
  /* Each card animates independently as it enters the viewport */
  animation: cardReveal linear;
  animation-timeline: view();
  animation-range: entry 0% entry 80%;
  
  /* Start invisible and off-screen */
  opacity: 0;
  transform: translateY(40px);
}

@keyframes cardReveal {
  from {
    opacity: 0;
    transform: translateY(40px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Staggered effect via animation-delay doesn't work with scroll timelines.
   Instead, use animation-range to create staggering */
.card:nth-child(1) { animation-range: entry 0% entry 80%; }
.card:nth-child(2) { animation-range: entry 10% entry 90%; }
.card:nth-child(3) { animation-range: entry 20% entry 100%; }
.card:nth-child(4) { animation-range: entry 30% entry 100%; }
.card:nth-child(5) { animation-range: entry 40% entry 100%; }
.card:nth-child(6) { animation-range: entry 50% entry 100%; }

Each card fades in and slides up as it scrolls into view. The staggered animation-range values create a cascading effect without any JavaScript.

Practical Example: Parallax Hero Section

Parallax effects are notoriously tricky to implement smoothly. With scroll timelines, they become straightforward:

<section class="hero">
  <div class="hero-background"></div>
  <div class="hero-content">
    <h1>Welcome to the Future of CSS</h1>
    <p>Scroll-driven animations are here.</p>
  </div>
</section>
<section class="main-content">
  <!-- Rest of the page -->
</section>
/* CSS */
.hero {
  position: relative;
  height: 100vh;
  overflow: hidden;
}

.hero-background {
  position: absolute;
  inset: 0;
  background: url('hero-image.jpg') center/cover;
  /* Parallax: moves slower than scroll, creating depth */
  animation: parallaxMove linear;
  animation-timeline: scroll(root);
  /* The background moves from -20% to 20% as user scrolls */
  --parallax-start: -15%;
  --parallax-end: 10%;
  transform: translateY(var(--parallax-start));
}

.hero-content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100%;
  
  /* Content fades out as user scrolls past the hero */
  animation: fadeOutContent linear;
  animation-timeline: scroll(root);
  animation-range: exit 0% exit 100%;
}

@keyframes parallaxMove {
  from {
    transform: translateY(-15%);
  }
  to {
    transform: translateY(10%);
  }
}

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

The background image moves at a different rate than the foreground content, creating a convincing parallax depth effect. The hero content gracefully fades out as the user scrolls into the main content area.

Practical Example: Horizontal Scroll Gallery with Snap Points

Combine CSS scroll snap with scroll timelines for an interactive gallery:

<div class="gallery-scroller">
  <div class="gallery-track">
    <div class="gallery-item">
      <img src="photo1.jpg" alt="">
      <span class="caption">Mountain Sunrise</span>
    </div>
    <div class="gallery-item">
      <img src="photo2.jpg" alt="">
      <span class="caption">Coastal Fog</span>
    </div>
    <div class="gallery-item">
      <img src="photo3.jpg" alt="">
      <span class="caption">Desert Bloom</span>
    </div>
    <div class="gallery-item">
      <img src="photo4.jpg" alt="">
      <span class="caption">Forest Canopy</span>
    </div>
  </div>
</div>
/* CSS */
.gallery-scroller {
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  /* Define the timeline on the horizontal scroll axis */
  scroll-timeline: --gallery-progress inline;
}

.gallery-track {
  display: flex;
  width: max-content;
}

.gallery-item {
  width: 80vw;
  flex-shrink: 0;
  scroll-snap-align: center;
  position: relative;
  overflow: hidden;
}

.gallery-item img {
  width: 100%;
  height: 60vh;
  object-fit: cover;
  /* Images scale up as they come into snap position */
  animation: scaleIn linear;
  animation-timeline: view(inline);
  animation-range: entry 0% entry 100%;
}

.caption {
  position: absolute;
  bottom: 20px;
  left: 20px;
  color: white;
  font-size: 1.5rem;
  /* Captions slide up from below */
  animation: slideUpCaption linear;
  animation-timeline: view(inline);
  animation-range: entry 40% entry 100%;
  opacity: 0;
  transform: translateY(20px);
}

@keyframes scaleIn {
  from {
    transform: scale(0.85);
    filter: brightness(0.6);
  }
  to {
    transform: scale(1);
    filter: brightness(1);
  }
}

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

This gallery uses view(inline) on each item so animations trigger as items scroll horizontally into the snap position. The images brighten and scale up, while captions slide in from below.

Advanced: Combining Multiple Timelines with animation-range

The animation-range property is where scroll-driven animations truly shine. It allows you to map specific portions of the scroll timeline to different phases of an animation. The property accepts named range phases:

/* A complex reveal with multiple animation phases */
.card {
  animation-name: scaleUp, fadeIn, glowPulse;
  animation-timeline: view(), view(), view();
  animation-range: 
    entry 0% entry 60%,    /* scaleUp: early entry */
    entry 20% entry 100%,  /* fadeIn: later entry */
    contain 0% contain 100%; /* glowPulse: while fully visible */
}

This creates a layered effect: the card scales up first, then fades in opacity, and once fully visible, a subtle glow pulses throughout the containment phase.

Best Practices for Scroll Timeline

Combining with JavaScript for Advanced Control

While scroll timelines eliminate most animation JavaScript, you may still want programmatic control for things like pausing, seeking, or logging. The Web Animations API exposes scroll-driven animations just like time-based ones:

// Access a CSS-declared scroll-driven animation
const element = document.querySelector('.card');
const animations = element.getAnimations();

animations.forEach(animation => {
  // Check if it's scroll-driven
  if (animation.timeline && animation.timeline instanceof ScrollTimeline) {
    console.log('Scroll timeline detected');
    console.log('Current progress:', animation.effect.getComputedTiming().progress);
    
    // You can pause and play
    animation.pause();
    animation.play();
    
    // Seek to a specific progress point (0 to 1)
    animation.currentTime = animation.timeline.duration * 0.5;
  }
});

// Create a scroll-driven animation entirely in JS
const scrollTimeline = new ScrollTimeline({
  source: document.querySelector('.scroll-container'),
  axis: 'block',
  // Optional: define scroll offsets
  scrollOffsets: [
    { target: document.querySelector('.start-marker'), edge: 'start' },
    { target: document.querySelector('.end-marker'), edge: 'end' }
  ]
});

const keyframes = [
  { opacity: 0, transform: 'translateY(50px)' },
  { opacity: 1, transform: 'translateY(0)' }
];

element.animate(keyframes, {
  timeline: scrollTimeline,
  fill: 'both',
  rangeStart: 'entry 0%',
  rangeEnd: 'entry 100%'
});

The JavaScript API is particularly useful for dynamic scenarios where scroll containers or elements are added after page load, or when you need to synchronize scroll-driven animations with other interactive systems like WebGL renderers or canvas updates.

Performance Considerations and Debugging

Scroll-driven animations run on the compositor thread, but certain property combinations can force layout recalculations. Use Chrome DevTools' Performance panel with the "Scrolling" category enabled to identify bottlenecks. The Rendering panel's "Layer borders" and "Paint flashing" tools help visualize which elements are being repainted unnecessarily.

Key performance rules:

Conclusion

CSS Scroll Timeline represents a fundamental shift in how we approach scroll-based interactivity on the web. By moving scroll-driven animations from JavaScript to the browser's compositor, we gain smoother performance, simpler code, and better accessibility — all while reducing our dependency on third-party libraries. The scroll() and view() functions, combined with animation-range and named timelines, give developers a declarative, expressive toolkit for creating everything from reading progress bars to parallax heroes and staggered card reveals. As browser support continues to expand, scroll-driven animations are poised to become a cornerstone of modern web design. Start experimenting today, keep your fallbacks robust, and embrace the scroll.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles