ScrollTimeline: Driving WAAPI Animations With Scroll Position
The Scroll-linked Animations specification defines a way to tie animation progress directly to scroll position. As a user scrolls a container, the animation advances and rewinds in lockstep. While the CSS side of the spec—via @scroll-timeline and animation-timeline—has gotten attention, the specification also defines a JavaScript interface built around the ScrollTimeline class, which plugs into the Web Animations API (WAAPI).
A WAAPI Refresher
WAAPI lets you construct and control animations entirely from JavaScript. Suppose you want a bar fixed at the top of the page to change color from red to darkred, while scaling its x-axis from zero to full width. The WAAPI equivalent looks like this:
new Animation(
new KeyframeEffect(
document.querySelector('.progressbar'),
{
backgroundColor: ['red', 'darkred'],
transform: ['scaleX(0)', 'scaleX(1)'],
},
{
duration: 2500,
fill: 'forwards',
easing: 'linear',
}
)
).play();
The shorter Element.animate() syntax achieves the same result:
document.querySelector('.progressbar').animate(
{
backgroundColor: ['red', 'darkred'],
transform: ['scaleX(0)', 'scaleX(1)'],
},
{
duration: 2500,
fill: 'forwards',
easing: 'linear',
}
);
Both snippets expose two distinct pieces. First is the keyframes object describing which properties to animate:
{
backgroundColor: ['red', 'darkred'],
transform: ['scaleX(0)', 'scaleX(1)'],
}
Second is the options object configuring the animation duration, easing, and other settings:
{
duration: 2500,
fill: 'forwards',
easing: 'linear',
}
Configuring a ScrollTimeline
To swap the default time-driven clock for scroll position, you attach a ScrollTimeline instance to the animation. The class constructor accepts three options:
source: The scrollable element whose scrolling activates and drives the timeline. Defaults todocument.scrollingElement, meaning the whole document.orientation: Which scroll direction triggers the timeline:verticalorblockby default.scrollOffsets: The scroll positions, in the specified orientation, that define the active range of the timeline. The animation progresses evenly between these offsets.
Those arguments are passed to the constructor directly:
const myScrollTimeline = new ScrollTimeline({
source: document.scrollingElement,
orientation: 'block',
scrollOffsets: [
new CSSUnitValue(0, 'percent'),
new CSSUnitValue(100, 'percent'),
],
});
Unsurprisingly, these options mirror the descriptors of the CSS @scroll-timeline at-rule. The two approaches are functionally equivalent; the only difference is the syntax.
With a ScrollTimeline instance in hand, pass it as the second argument to the Animation constructor:
new Animation(
new KeyframeEffect(
document.querySelector('#progress'),
{ transform: ['scaleX(0)', 'scaleX(1)'], },
{ duration: 1, fill: 'forwards' }
),
myScrollTimeline
).play();
Or specify it as the timeline property in the options object for Element.animate():
document.querySelector("#progress").animate(
{
transform: ["scaleX(0)", "scaleX(1)"]
},
{
duration: 1,
fill: "forwards",
timeline: myScrollTimeline
}
);
The animation now runs off scroll position rather than the standard DocumentTimeline.
Note that Chromium’s experimental implementation currently expects scrollSource instead of source. The code examples therefore include both properties.
Browser Support and Polyfilling
As of this writing, only Chromium browsers support ScrollTimeline, and only behind a feature flag. For wider reach, Robert Flack’s Scroll-Timeline Polyfill fills the gap, and all demos in the original article include it. The polyfill self-registers when it detects no native support, and also loads the required CSS Typed Object Model classes if the browser lacks them. Adding it is a single import:
import 'https://flackr.github.io/scroll-timeline/dist/scroll-timeline.js';
Element-Based Offsets
Beyond absolute pixel positions, the spec supports element-based scroll offsets. As the specification’s own description puts it:
With this type of Scroll Offsets the animation is based on the location of an element within the scroll-container. Typically this is used to animate an element as it comes into the scrollport until it has left the scrollport; e.g. while it is intersecting.
An element-based offset is defined by three components:
target: The DOM element being tracked.edge: The boundary of the scroll container that the target must cross.threshold: A value from0.0to1.0indicating how much of the target must intersect with theedge, similar toIntersectionObserver.
In JavaScript, define such an offset with a plain object:
{
target: document.querySelector('#targetEl'),
edge: 'end',
threshold: 0.5,
}
Typically you pass two of these objects to the scrollOffsets array:
const $image = document.querySelector('#myImage');
$image.animate(
{
opacity: [0, 1],
clipPath: ['inset(45% 20% 45% 20%)', 'inset(0% 0% 0% 0%)'],
},
{
duration: 1,
fill: "both",
timeline: new ScrollTimeline({
scrollSource: document.scrollingElement,
timeRange: 1,
fill: "both",
scrollOffsets: [
{ target: $image, edge: 'end', threshold: 0.5 },
{ target: $image, edge: 'end', threshold: 1 },
],
}),
}
);
This pattern drives the image reveal effect where an image fades in and unmasks as it scrolls into the viewport.
Other ScrollTimeline Demos
A horizontal scroll section can also be driven by ScrollTimeline, reusing an approach originally built on GSAP’s ScrollTrigger. A related demo recreates iTunes’ CoverFlow effect, although a known Chromium bug miscomputes the start and end positions in that particular build.
CSS or JavaScript?
The CSS and JavaScript routes to scroll-linked animations share the same underlying concepts, differing mainly in language. For a progressive-enhancement approach, CSS would generally be preferred—except that browser support is still thin at the moment:
- Chromium ships it behind a feature flag.
- Firefox is preparing implementation work (Mozilla Ticket #1676780).
- Safari has not yet signaled support (WebKit Ticket #222295).
Given those constraints, the JavaScript-based ScrollTimeline path is currently the more practical option—just ensure content remains accessible and readable if JavaScript is unavailable.



