Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimeline

The Scroll-linked Animations specification is an upcoming and experimental addition that allows us to link animation-progress to scroll-progress: as you scroll up and down a scroll container, a linked animation also advances or rewinds accordingly.
We covered some use cases in a previous piece here on CSS-Tricks, all driven by the CSS @scroll-timeline at-rule and animation-timeline property the specification provides — yes, that’s correct: all those use cases were built using only HTML and CSS. No JavaScript.
Apart from the CSS interface we get with the Scroll-linked Animations specification, it also describes a JavaScript interface to implement scroll-linked animations. Let’s take a look at the ScrollTimeline class and how to use it with the Web Animations API.

Web Animations API: A quick recap
The Web Animations API (WAAPI) has been covered here on CSS-Tricks before. As a small recap, the API lets us construct animations and control their playback with JavaScript.
Take the following CSS animation, for example, where a bar sits at the top of the page, and:
animates from red to darkred, thenanimates from zero width to full-width (by scaling the x-axis).CodePen Embed Fallback
Translating the CSS animation to its WAAPI counterpart, the code becomes this:
new Animation(
new KeyframeEffect(
document.querySelector(‘.progressbar’),
{
backgroundColor: [‘red’, ‘darkred’],
transform: [‘scaleX(0)’, ‘scaleX(1)’],
},
{
duration: 2500,
fill: ‘forwards’,
easing: ‘linear’,
}
)
).play();
CodePen Embed Fallback
Or alternatively, using a shorter syntax with Element.animate():
document.querySelector(‘.progressbar’).animate(
{
backgroundColor: [‘red’, ‘darkred’],
transform: [‘scaleX(0)’, ‘scaleX(1)’],
},
{
duration: 2500,
fill: ‘forwards’,
easing: ‘linear’,
}
);
CodePen Embed Fallback
In those last two JavaScript examples, we can distinguish two things. First, a keyframes object that describes which properties to animate:
{
backgroundColor: [‘red’, ‘darkred’],
transform: [‘scaleX(0)’, ‘scaleX(1)’],
}
Second is an options Object that configures the animation duration, easing, etc.:
{
duration: 2500,
fill: ‘forwards’,
easing: ‘linear’,
}
Creating and attaching a scroll timeline
To have our animation be driven by scroll — instead of the monotonic tick of a clock — we can keep our existing WAAPI code, but need to extend it by attaching a ScrollTimeline instance to it.
This ScrollTimeline class allows us to describe an AnimationTimeline whose time values are determined not by wall-clock time, but by the scrolling progress in a scroll container. It can be configured with a few options:
source: The scrollable element whose scrolling triggers the activation and drives the progress of the timeline. By default, this is document.scrollingElement (i.e. the scroll container that scrolls the entire document).orientation: Determines the direction of scrolling, which triggers the activation and drives the progress of the timeline. By default, this is vertical (or block as a logical value).scrollOffsets: These determine the effective scroll offsets, moving in the direction specified by the orientation value. They constitute equally-distanced in progress intervals in which the timeline is active.These options get passed into the constructor. For example:
const myScrollTimeline = new ScrollTimeline({
source: document.scrollingElement,
orientation: ‘block’,
scrollOffsets: [
new CSSUnitValue(0, ‘percent’),
new CSSUnitValue(100, ‘percent’),
],
});
It’s not a coincidence that these options are exactly the same as the CSS @scroll-timeline descriptors. Both approaches let you achieve the same result with the only difference being the language you use to define them.
To attach our newly-created ScrollTimeline instance to an animation, we pass it as the second argument into the Animation constructor:
new Animation(
new KeyframeEffect(
document.querySelector(‘#progress’),
{ transform: [‘scaleX(0)’, ‘scaleX(1)’], },
{ duration: 1, fill: ‘forwards’ }
),
myScrollTimeline
).play();
CodePen Embed Fallback
When using the Element.animate() syntax, set it as the timeline option in the options object:
document.querySelector(“#progress”).animate(
{
transform: [“scaleX(0)”, “scaleX(1)”]
},
{
duration: 1,
fill: “forwards”,
timeline: myScrollTimeline
}
);
CodePen Embed Fallback
With this code in place, the animation is driven by our ScrollTimeline instance instead of the default DocumentTimeline.
The current experimental implementation in Chromium uses scrollSource instead of source. That’s the reason you see both source and scrollSource in the code examples.
A word on browser compatibility
At the time of writing, only Chromium browsers support the ScrollTimeline class, behind a feature flag. Thankfully there’s the Scroll-Timeline Polyfill by Robert Flack that we can use to fill the unsupported gaps in all other browsers. In fact, all of the demos embedded in this article include it.
The polyfill is available as a module and registers itself if no support is detected. To include it, add the following import statement to your JavaScript code:
import ‘https://flackr.github.io/scroll-timeline/dist/scroll-timeline.js’;
The polyfill also registers the required CSS Typed Object Model classes, should the browser not support it. (👀 Looking at you, Safari.)
Advanced scroll timelines
Apart from absolute offsets, scroll-linked animations can also work with element-based offsets:
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 consists of three parts that describe it:
target: The tracked DOM element.edge: This is what the ScrollTimeline’s source watches for the target to cross.threshold: A number ranging from 0.0 to 1.0 that indicates how much of the target is visible in the scroll port at the edge. (You might know this from IntersectionObserver.)Here’s a visualization:
CodePen Embed Fallback
If you want to know more about element-based offsets, including how they work, and examples of commonly used offsets, check out this article.
Element-based offsets are also supported by the JS ScrollTimeline interface. To define one, use a regular object:
{
target: document.querySelector(‘#targetEl’),
edge: ‘end’,
threshold: 0.5,
}
Typically, you pass two of these objects into the scrollOffsets property.
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 code is used in the following demo below. It’s a JavaScript-remake of the effect I covered last time: as an image scrolls into the viewport, it fades-in and becomes unmasked.
CodePen Embed Fallback
More examples
Here are a few more examples I cooked up.
Horizontal scroll section
This is based on a demo by Cameron Knight, which features a horizontal scroll section. It behaves similarly, but uses ScrollTimeline instead of GSAP’s ScrollTrigger.
CodePen Embed Fallback
For more on how this code works and to see a pure CSS version, please refer to this write-up.
CoverFlow
Remember CoverFlow from iTunes? Well, here’s a version built with ScrollTimeline:
CodePen Embed Fallback
This demo does not behave 100% as expected in Chromium due to a bug. The problem is that the start and end positions are incorrectly calculated. You can find an explanation (with videos) in this Twitter thread.
More information on this demo can be found in this article.
CSS or JavaScript?
There’s no real difference using either CSS or JavaScript for the Scroll-linked Animations, except for the language used: both use the same concepts and constructs. In the true spirit of progressive enhancement, I would grab to CSS for these kind of effects.
However, as we covered earlier, support for the CSS-based implementation is fairly poor at the time of writing:
Because of that poor support, you’ll certainly get further with JavaScript at this very moment. Just make sure your site can also be viewed and consumed when JavaScript is disabled. 😉

Typed at

Share your love

Leave a Reply