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, here it is. You can see this code in action in the demos.
const trackAnimationProgress = (animation, cb, precision = 5) => {
let progress = null;
const updateValue = () => {
if (animation && animation.currentTime) {
let newProgress = animation.effect.getComputedTiming().progress * 1;
if (animation.playState === "finished") newProgress = 1;
newProgress = Math.max(0.0, Math.min(1.0, newProgress)).toFixed(precision);
if (newProgress != progress) {
progress = newProgress;
cb(progress);
}
}
requestAnimationFrame(updateValue);
};
requestAnimationFrame(updateValue);
}
document.querySelectorAll('.subject').forEach($subject => {
trackAnimationProgress($subject.getAnimations()[0], (progress) => {
// Do something with progress
});
});
~
# 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.
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)
~
# 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 trackAnimationProgress
helper:
trackAnimationProgress(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");
trackAnimationProgress(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.
~
# 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.