Scroll-driven animations are now Baseline Newly available. Firefox 159, released on 27 October 2026, turned them on by default, joining Chrome and Safari, so the feature works in the current version of every major browser. That means reading progress bars, reveal-on-scroll effects and simple parallax can now be written in plain CSS, with no scroll listeners and no animation library. This tutorial builds three common effects step by step and shows how to ship them responsibly.
The idea in one sentence
A normal CSS animation runs against time. A scroll-driven animation runs against scrolling: the animation’s progress is tied to how far a container has scrolled, or to where an element sits in the viewport. In the words of the Firefox 159 release notes, the feature lets you “animate property values along a scroll-based timeline … rather than along the default time-based document timeline.”
There are two kinds of timeline:
- Scroll progress timeline – follows how far a scroll container has been scrolled. Use
animation-timeline: scroll()or thescroll-timelineproperty. - View progress timeline – follows an element as it moves through the visible area of its scroller. Use
animation-timeline: view()or theview-timelineproperty.
Example 1: A reading progress bar
This is the classic scroll progress timeline. First the HTML:
<div class="progress" aria-hidden="true"></div>
<article>
<h1>My long article</h1>
<p>…</p>
</article>
Then the CSS:
.progress {
position: fixed;
inset: 0 0 auto 0;
height: 4px;
background: #4f46e5;
transform-origin: 0 50%;
transform: scaleX(0);
animation: grow-progress linear both;
animation-timeline: scroll(root block);
}
@keyframes grow-progress {
to { transform: scaleX(1); }
}
How it works:
scroll(root block)means “use the root (page) scroller, along the block axis” (vertical for most pages).- At the top of the page the animation is at 0%; at the bottom it is at 100%.
- You do not set a duration, because time is no longer what drives the animation.
linearkeeps the bar in step with the scroll position. - Animating
transforminstead ofwidthkeeps it cheap for the browser to render.
Example 2: Fade cards in as they enter the viewport
A view progress timeline tracks each element individually, which is perfect for reveal effects.
<section class="cards">
<div class="card">One</div>
<div class="card">Two</div>
<div class="card">Three</div>
</section>
.card {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 30%;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(40px) scale(0.96);
}
to {
opacity: 1;
transform: none;
}
}
The important line is animation-range. It limits the animation to part of the element’s journey through the viewport:
entry 0%– start as soon as the card begins to enter.cover 30%– finish when the card has travelled 30% of the way through the “cover” range.
Without a range, the animation would stretch across the whole time the card is visible, and the card would only be fully opaque at the moment it leaves the screen.
Example 3: A named timeline for a horizontal gallery
Sometimes the element you animate is not inside the scroller. Named timelines solve that: you declare the timeline on the scroller and reference it elsewhere.
<div class="gallery-wrap">
<div class="gallery">
<img src="1.jpg" alt="…"> <img src="2.jpg" alt="…"> <img src="3.jpg" alt="…">
</div>
<div class="gallery-progress"></div>
</div>
.gallery-wrap {
timeline-scope: --gallery;
}
.gallery {
display: flex;
gap: 1rem;
overflow-x: auto;
scroll-timeline: --gallery inline;
}
.gallery-progress {
height: 3px;
background: currentColor;
transform-origin: 0 50%;
animation: grow-progress linear both;
animation-timeline: --gallery;
}
scroll-timeline: --gallery inline names the horizontal scroll position of .gallery. Because the progress bar is a sibling, not a child, timeline-scope on the common parent makes the name visible to it. The grow-progress keyframes are reused from example 1.
Tip: named timelines and timeline-scope are newer than the basic scroll() and view() functions. Check the browser compatibility table on MDN before relying on them, and keep the simpler patterns from examples 1 and 2 as your default.
Shipping it responsibly
1. Respect reduced motion
Scroll-driven animations are still motion. Wrap decorative effects so visitors who ask for less motion do not get them:
@media (prefers-reduced-motion: no-preference) {
.card {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 30%;
}
}
2. Make the page work without it
“Baseline Newly available” means the current versions of major browsers support it. Visitors on older versions will not see the effect, so design the default state to be readable. In example 2, the cards are fully visible unless the animation runs. A feature query keeps older browsers safe:
@supports (animation-timeline: view()) {
/* scroll-driven rules here */
}
3. Animate cheap properties
Stick to transform and opacity where you can. Animating layout properties such as width, top or height on scroll forces extra layout work on every frame.
4. Delete the JavaScript you no longer need
Search your codebase for addEventListener('scroll' and IntersectionObserver code that only toggles classes for visual effects. Many of those can now be replaced by a few lines of CSS, which means less code running on the main thread while users scroll.
Also new in Firefox 159
- Scoped custom element registries are supported. A shadow tree can create its own
CustomElementRegistry, so two web components that define the same tag name no longer collide. - The legacy masonry syntax (
grid-template-rows: masonry) was removed. It was only enabled by default in Nightly and is superseded by grid lanes layout (display: grid-lanes).
Key takeaway
With Firefox 159, scroll-driven animations work in every major browser. Use animation-timeline: scroll() for progress indicators, view() with animation-range for reveal effects, and named timelines for anything more complex. Guard motion with prefers-reduced-motion, keep a sensible default for older browsers, and retire scroll-listener JavaScript. The MDN guide to scroll-driven animations covers every property in detail.
