Scroll-Driven Animations: scroll-timeline and view-timeline Without JavaScript
AI generated
tw
Tailwind CSS · Scroll-Driven Animations · view-timeline · Performance
Scroll-Driven Animations
scroll-timeline and view-timeline without JavaScript

A progress bar that fills as you scroll, or an image with a parallax effect, used to be built almost exclusively with a JavaScript scroll listener recalculating on every scroll event. With scroll-timeline and view-timeline, the browser handles this natively, in sync with the compositor thread, with no script at all.

16 min read scroll-timeline · view-timeline Scroll-driven animations, no JS

1. The problem with JavaScript scroll listeners for animation

Scroll-bound effects such as a reading progress bar, a parallax background, or an element that fades in slowly as it scrolls into view have almost always been built with a scroll event listener in JavaScript that reads the current scroll position on every scroll step and computes a style value from it. That approach works, but it runs on the browser's main thread, the same thread responsible for layout calculations, script execution, and many other tasks, so frequent scroll event firing can cause noticeable jank, especially on lower-powered devices.

Even with optimization techniques like throttling, debouncing, or using requestAnimationFrame, the fundamental problem remains that the animation runs on the main thread and is therefore dependent on how busy it is. Native CSS scroll animations through scroll-timeline and view-timeline solve this by having the browser tie the animation directly to scroll position and compute it on the more performant compositor thread, regardless of what the main thread happens to be doing.

2. scroll-timeline basics

The CSS scroll-timeline property defines a named timeline whose progress, unlike classic CSS animations, doesn't depend on elapsed time but on the scroll progress of a given scroll container. An element with scroll-timeline-name: --page-scroll and scroll-timeline-axis: block on the scrolling container produces a timeline that runs from zero percent at the start of the scrollable area to one hundred percent at the end.

An animation tied to this timeline no longer runs over a fixed duration in seconds, but over the container's entire scroll progress instead, driven through the property animation-timeline: --page-scroll. That means the classic animation-duration value plays practically no role anymore for scroll-bound animations, since progress calculation is now handled entirely by scroll state instead of time.

3. Practical example: a progress bar that fills while scrolling

A reading progress bar at the top of the screen, showing how far a user has scrolled through an article, is one of the most common use cases for scroll-timeline. Instead of a JavaScript listener setting a bar's width through style.width on every scroll event, a plain CSS keyframe animation that transforms width from zero to one hundred percent, tied to the document's scroll position through scroll-timeline, handles the exact same job.

In the example below, the timeline is defined on the root element and a progress bar is tied to it through a simple keyframe animation. The key difference from the JavaScript solution is that the browser handles the width calculation on every single scroll frame, without a single byte of JavaScript needing to be loaded for it.


:root {
  scroll-timeline-name: --page-scroll;
  scroll-timeline-axis: block;
}

.progress-bar {
  transform-origin: left;
  animation: fill-progress linear;
  animation-timeline: --page-scroll;
}

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

4. view-timeline basics for element visibility

While scroll-timeline maps the progress of an entire scroll container, the related view-timeline property addresses a different, very common use case: an animation that runs exactly while a specific element passes through the visible viewport area. An element with view-timeline-name: --card-reveal produces a timeline whose progress tracks how far that specific element is currently visible in the viewport, regardless of how long the overall page is.

This property is a great fit for classic reveal animations, where individual cards or sections fade in smoothly or shift slightly as they scroll into view, an effect that used to be implemented almost exclusively with an IntersectionObserver in JavaScript adding a CSS class once a threshold is crossed. With view-timeline, that JavaScript detour disappears entirely, since the browser itself detects when and how far an element is visible.

5. Parallax effects with view-timeline

A classic parallax effect, where a background image moves more slowly than the rest of the page content, can be built with view-timeline by having an image element run through a subtle, continuous transform for its entire time in the viewport, for example a vertical shift from minus ten to plus ten percent. Because the animation is tied directly to the element's actual visibility, the effect stays exactly in sync with scroll behavior, without the slight lag JavaScript-based parallax libraries can produce from processing events on the main thread.

This technique pays off especially in hero sections with large images, since it needs no additional JavaScript library and still stays jank-free even on devices with limited compute, because the calculation runs on the compositor thread and doesn't compete with other script execution for processing time. It's worth keeping transform values moderate, since overly strong parallax shifts can cause discomfort for some users, so a prefers-reduced-motion media query is worth adding here too.

6. The animation-range property in detail

The animation-range property determines the exact slice of the timeline over which an animation actually plays, rather than being forced to use the full timeline from zero to one hundred percent. With view-timeline, named ranges such as entry, contain, and exit are available, each describing the phase during which an element is entering the viewport, is fully visible, or is leaving it again.

A value like animation-range: entry 0% cover 40% makes the animation play only during the element's entry phase and hold at its final state afterward, which is typically what's wanted for reveal effects, since an element should only fade in once on first appearance and not re-animate later when it leaves the viewport and the user scrolls back. This fine-grained control makes animation-range one of the most powerful tools for precisely choreographed scroll effects.

7. Progressive enhancement with @supports and Tailwind utility classes

Since scroll-driven animations layer purely additively on top of existing CSS animations, their use can be cleanly guarded with @supports (animation-timeline: view()), activating scroll-bound behavior only where the browser genuinely supports it, while unsupported browsers simply leave the element in its starting state or receive a simple, time-based fallback animation. In Tailwind projects, this condition can be encapsulated in a small custom utility class that bundles the scroll-timeline declarations and gets applied in markup just like any other utility class.

This combination of a native CSS capability and a utility-class structure fits well with the Tailwind approach, since recurring scroll animation patterns, such as a card reveal or a progress bar, can be defined as small, reusable classes and then wired into markup just as declaratively as any other Tailwind class, instead of writing bespoke scroll logic for every individual component.

8. Current browser support status for scroll-driven animations

Chrome and Edge have fully supported scroll-timeline and view-timeline since version 115 on Blink, while Firefox and Safari have not yet fully completed their implementation at the time of writing, making this one of the least cross-browser-ready CSS capabilities covered in this series. Anyone deploying scroll-bound animations in production should therefore lean on progressive enhancement even more heavily right now than with the other features covered here.

A solid pattern is to leave the element in a sensible, static state when support is missing, for instance a card that's already fully visible rather than an invisible one that never fades in, since without @supports guarding, an element could in the worst case stay permanently transparent. This defensive default, combined with regularly checking the current support status, already makes scroll-driven animations usable in production today without disadvantaging users on unsupported browsers.

9. Best practices and performance notes

A key performance advantage of scroll-driven animations over JavaScript solutions is that the calculation runs on the compositor thread and therefore stays independent of main-thread load, which makes a noticeable difference especially on pages with a lot of concurrently running JavaScript, such as content-heavy product pages full of Alpine.js components. The catch is that only compositor-friendly properties like transform and opacity should be animated, since animating layout-affecting properties like width or top still triggers layout recalculation even with scroll-driven animations.

It's also worth deliberately capping how many scroll timelines run simultaneously on a page, since too many parallel reveal effects, for instance across a very long list of cards, can make a page feel visually overwhelming even if raw compute power would be sufficient. As with @starting-style, the right approach is to animate a handful of meaningful elements deliberately rather than giving every component on the page its own scroll effect, and to always plan for a prefers-reduced-motion check for users sensitive to motion.

Property Reference point Typical use Browser support
scroll-timeline Scroll progress of a container Reading progress bar, page progress Chrome/Edge since v115, Firefox/Safari in progress
view-timeline Visibility of a single element Card reveal, fade-in on scroll into view Chrome/Edge since v115, Firefox/Safari in progress
animation-range Slice of the timeline (entry/exit/contain) Precise control over the animation's start and end Tied to scroll-timeline/view-timeline
JavaScript scroll listener Manually computed scroll position Fallback for unsupported browsers Universal, but tied to the main thread

Mironsoft

Tailwind CSS architecture, design systems, and performance

Tailwind frontends that stay maintainable despite thousands of utility classes?

We review existing Tailwind projects for bloated class lists, inconsistent design tokens, and unused CSS remnants, then build a design system that scales cleanly instead of getting messier with every component.

Design System Review

Checking tokens, spacing scale, and component consistency for maintainability.

Performance Optimization

Systematically reducing CSS bundle size, purge configuration, and load times.

Component Architecture

Building reusable, well-structured components instead of sprawling class lists.

10. Summary

Scroll-driven animations at a glance

scroll-timeline

Ties an animation to a container's scroll progress, ideal for progress bars.

view-timeline

Ties an animation to a single element's visibility inside the viewport.

animation-range

Precisely controls which phase (entry, contain, exit) the animation plays in.

Browser support

Chromium-based for now, @supports guarding needed for Firefox/Safari.

11. FAQ: Scroll-driven animations at a glance

1What is the difference between scroll-timeline and view-timeline?
scroll-timeline ties an animation to the scroll progress of an entire container, while view-timeline ties an animation to the visibility of a single element inside the viewport, regardless of the page's overall length.
2Do I still need a JavaScript listener for a scroll progress bar?
No, with scroll-timeline and a CSS keyframe animation, a progress bar can be built entirely declaratively, with no scroll event listener needed to set its width through a script.
3What is the animation-range property needed for?
It determines the exact slice of the timeline an animation actually plays over, for instance only during an element's entry phase, rather than being forced to use the full timeline from zero to one hundred percent.
4Why are scroll-driven animations more performant than JavaScript solutions?
The calculation runs on the browser's compositor thread instead of the main thread, keeping the animation independent of load from script execution and layout calculations.
5Which browsers already support scroll-timeline and view-timeline?
Chrome and Edge have fully supported both properties since version 115, while Firefox and Safari have not yet fully completed their implementation at the time of writing.
6How can I use scroll-driven animations as progressive enhancement?
An @supports check for animation-timeline: view() activates scroll-bound behavior only where the browser supports it, while other browsers leave the element in a sensible static state instead.
7Is view-timeline a good fit for parallax effects?
Yes, an image element can run a subtle, continuous transform for its entire time in the viewport, producing a parallax effect exactly in sync with scroll behavior without any JavaScript.
8Which CSS properties should I animate in scroll-driven animations?
transform and opacity are the most performant, since the compositor thread handles them without layout recalculation, while properties like width or top still trigger expensive layout recalculation.
9What happens to an element if a browser doesn't support scroll-driven animations?
Without @supports guarding, an element could stay permanently in its transparent starting state, which is why a sensible, visible fallback state should be explicitly defined.
10Should I account for prefers-reduced-motion with scroll-driven animations?
Yes, as with any motion-based animation, a prefers-reduced-motion media query should reduce or fully disable these effects for users sensitive to motion.