Synchronize videos, 3D-models, etc. to Scroll-Driven Animations

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.

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;


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, your one-stop shop for all your Scroll-Driven Animations needs. The demo is located at 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:


Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Join the Conversation


Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.