With Scroll-Driven Animations it’s really easy to animate elements as they enter/exit/whatever. But what if you want to sync a video to that? Or maybe rotate a 3D-model as you scroll? With a little bit of JavaScript, it’s pretty easy to do so!
~
# The Inspiration
Earlier today I saw a tweet showing a recording of the 3D drag interaction on Polestar’s website making rounds.
Yo! Take a look at this fantastic 3D drag interaction on Polestar's website 🤯
In a single drag event:
→ Each car model element transforms in X axis
→ Pagination active state and number fades in/out
→ Car model info changes above🔗 https://t.co/YwJX9KUqyM pic.twitter.com/M5Fxt9CFwO
— Vaibhav Khulbe (@vaibhav_khulbe) June 20, 2023
As my colleague Adam Argyle cleverly noted, it’s nothing more than Scroll Snapping and Scroll-Driven Animations.
But how exactly can you achieve that Scroll-Driven Animation? It’s easy to animate elements as they enter/exit/whatever … but how can you control that 3D-model? That’s not possible with CSS/WAAPI based Scroll-Driven Animations, right? Not directly no, but but with a little bit of JavaScript you can extract the animation’s progress use that to synchronize the rotation of the 3D-model.
~
# The Code
If you’re here for just the code, you can import trackProgress
from the @bramus/sda-utilities
package.
import { trackProgress } from '@bramus/sda-utilities';
// Update text of the `.animation-subject` element with the effect progress
trackProgress(document.querySelector('.animation-subject').getAnimations()[0], (progress) => {
document.querySelector('.animation-subject').innerText = `${(progress * 100).toFixed(5)}%`;
});
To see this snippet in action in, go check out the demos.
The source code of the trackProgress
function itself is available on GitHub.
~
# How it works
At its core, the code is essentially this:
let progress = animation.effect.getComputedTiming().progress * 1;
if (animation.playState === "finished") progress = 1;
progress = Math.max(0.0, Math.min(1.0, progress)).toFixed(2);
It takes an animation
and then gets the progress
from the effect
. The value is clamped between 0
and 1
, taking its playState
into account as well. This logic is looped in a requestAnimationFrame
, only calling the callback
when the progress has changed.
Avid readers might wonder why I’m not reading animation.currentTime.value
directly. While it does give you the correct value for the full range of the timeline, it does not play nice with animation-range
.
For example, when an element with a ViewTimeline has a animation-range
set to entry
, only the effect’s progress will from 0% to 100% during the actual entry
segment. The animation
itself on the other hand does not take the range information into account, and will go from 0% to 100% over the entire cover
range.
You can see for yourself using the View Timeline Progress Visualizer I built. With the controls, choose a different range or change the range-start
+range-end
– you’ll see the numbers diverge.
In the future, it’ll be much easier as you’ll be able to read the progress directly from the animation through animation.progress
. This API – part of web-animations-2
– is not available in Chrome (or any other browser) yet (CRBUG)
Alternatively, Custom Effects could also solve this. Only Safari has an experimental implementation of it at the time of writing.
~
# Demos
1. A <video>
driven by scroll
While at CSS Day two weeks ago, I showed a demo where the playback of a <video>
element was synchronized to a View Timeline. As the hotpink
box crosses the screen, the video plays from start to finish.
See the Pen Control the playback of a <video> using Scroll-driven Animations by Bramus (@bramus) on CodePen.
The code uses the aforementioned trackProgress
helper:
trackProgress(document.querySelector(".animation-subject").getAnimations()[0], (progress) => {
document.querySelector(".animation-subject").innerText = `${(progress * 100).toFixed(5)}%`; // Update text content of the box
document.querySelector("video").currentTime = (document.querySelector("video").duration * progress).toFixed(5); // Update playhead of the video
});
~
2. A 3D-model rotating as you scroll
Using the same JavaScript helper, it’s really easy to control a 3D-model embedded using <model-viewer>
const model = document.querySelector("model-viewer");
trackProgress(model.getAnimations()[0], (progress) => {
model.orientation = `0deg 0deg ${progress * -360}deg`;
});
See the Pen 3D Model Scroll-Driven Animation by Bramus (@bramus) on CodePen.
This demo uses a ScrollTimeline on the body
~
3. Multiple 3D-models rotating
To more closely mimic the original demo, I added a more expansive demo to https://scroll-driven-animations.style/, your one-stop shop for all your Scroll-Driven Animations needs. The demo is located at https://scroll-driven-animations.style/demos/3d-shoe-explorer/css/ and also embedded below.
A recording of it, is embedded at the top of this post.
~
# A note on performance
Note that this code to determine the progress
is looped in a requestAnimationFrame
. The code has been optimized to only invoke the callback
when the progress
value has actually changed, but that doesn’t change the fact that it tries to do something every frame.
Even only this small task – it’s only a few µs
– can have a hit on performance, as using requestAnimationFrame
will force composited animations to also run on the Main Thread again (in Chrome). The reason for this is that if you were to query a style value or the like in that rAF
callback you would need to see the correct value. Therefore all animations their currentTime need to be synced up and a full main thread style update needs to be done.
A progress
event on the animation, or CustomEffect
s would not have this constant rAf
running, thereby being more performant.
~
# In Closing
As I’ve said before, Scroll-Driven Animations are really powerful. However, not all cases are covered by it. But thanks to a little bit of JavaScript, you can get it to reach into those edge cases and corners too 🙂
~
# Spread the word
To help spread the contents of this post, feel free to retweet its announcement tweet:
With #CSS Scroll-Driven Animations it’s really easy to animate elements as they enter/exit/whatever.
But what if you want to sync a video to that? Or maybe rotate a 3D-model as you scroll? Well, with a little bit of JavaScript, it’s pretty easy to do so!https://t.co/k4FDsCDEFk pic.twitter.com/pUbGJK1oiq
— Bram.us (@bramusblog) June 21, 2023
~
🔥 Like what you see? Want to stay in the loop? Here's how:
Would be nice if this solution had a fallback for non-chrome users.
There is a Scroll-Driven Animations polyfill you can use: https://github.com/flackr/scroll-timeline