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.
Table of Contents
- 1. The problem with JavaScript scroll listeners for animation
- 2. scroll-timeline basics
- 3. Practical example: a progress bar that fills while scrolling
- 4. view-timeline basics for element visibility
- 5. Parallax effects with view-timeline
- 6. The animation-range property in detail
- 7. Progressive enhancement with @supports and Tailwind utility classes
- 8. Current browser support status for scroll-driven animations
- 9. Best practices and performance notes
- 10. Summary
- 11. FAQ
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.