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
Table of Contents
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
todarkred
, then - animates from zero width to full-width (by scaling the x-axis).
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();
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', }
);
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 isdocument.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 isvertical
(orblock
as a logical value).scrollOffsets
: These determine the effective scroll offsets, moving in the direction specified by theorientation
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();
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 }
);
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 theScrollTimeline
’ssource
watches for thetarget
to cross.threshold
: A number ranging from0.0
to1.0
that indicates how much of thetarget
is visible in the scroll port at theedge
. (You might know this from IntersectionObserver.)
Here’s a visualization:
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.
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
.
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
:
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. ????