DEV Community

Cover image for A better way to build autoplaying feature videos.
Chris How
Chris How

Posted on • Updated on

A better way to build autoplaying feature videos.

As part of the team working the website for self-driving AI company Wayve, we needed a neat way to present an auto-playing video that:

  • Retained interactivity without asking much of the user
  • Started playing at the right time so the user didn't miss the beginning
  • Was a bit "stickier" than just a straight-up embedded video would be.

So we implemented an auto-playing "zoom-on-scroll" element.

Notice how the video of the London street scene automatically zooms to fill the window and starts playing when scrolled into view:

Scrolling the Wayve homepage

The video also sticks around a while as you scroll, drawing the user's attention to it. It's not a massive drag to get past it; you don't have to click anything to dismiss it. You just scroll a bit further and the video "un-zooms" and stops playing.

I've seen similar effects on other sites, but given that we were in a post-Internet Explorer world, I had an opportunity to implement it using modern techniques, leading to less jank, less code, and smoother animations.

So let's have a look at the steps needed to implement such a feature.

Step 1: Has the user scrolled the video into view?

The first step is to know when the user scrolls the video element into the viewport.

In the bad old days, we would calculate the vertical position of the video element, keep track of how far down the user has scrolled the page by trapping the scroll event, working out whether our video element was inside the viewport, and recalculate everything when the window was resized, or dynamic elements were added or removed from the page.

But now we can use the Intersection Observer API, which has been available on all modern browsers since 2019.

Here is an example of this pattern in use:

Stepping through this example, we first set the options for the observer. We're only interested in the threshold option: how much of the element must be inside the viewport for the observer to consider the element as inside.

We're going to plump for 1: consider the element to be 'inside the viewport' when 100% of it is inside the viewport (as opposed to — say — 75%).

const options = {
  threshold: 1 // 100% of element is in viewport
};
Enter fullscreen mode Exit fullscreen mode

Next we're going to add the callback for the intersection observer: what to do when an intersection change is observed.

const callback = (entries) => {
  entries.forEach(entry => {
    // Add class 'in-view' to element if
    // it is within the viewport
    entry.target.classList.toggle('in-view', entry.intersectionRatio === 1);
    });
};
Enter fullscreen mode Exit fullscreen mode

Next we are going to create an intersection observer with those options and that callback action:

const observer = new IntersectionObserver(callback, options);
Enter fullscreen mode Exit fullscreen mode

Note that this observer can observe any number of video elements, there's no need to create an observer for each element you want to observe; you can simply attach the same observer to as many video elements as you like.

Here we're just going to attach it to one element for simplicity, the element with the id observed:

observer.observe(document.getElementById('observed'));
Enter fullscreen mode Exit fullscreen mode

Step 2: Start the video playing

Now we want to automatically start and stop the video as the user scrolls it into view.

This is especially handy where you want a video to play without requiring the user to do anything onerous, but you don't want to start autoplaying when the page loads, as the user would miss the start of the video (it's below 'the fold'!)

So we're going to tweak the callback that's supplied to the Insersection Observer:

const callback = (entries) => {
  entries.forEach(entry => {
    // Find the video element inside this container
    const videoElement = entry.target.getElementsByTagName('video')[0];

    const isWithinViewport = entry.intersectionRatio === 1;

    // Add class 'in-view' to element if
    // it is within the viewport
    entry.target.classList.toggle('in-view', isWithinViewport);


    if(isWithinViewport) {
      // Play the video if it's within the viewport      
      videoElement.play();
    } else {
      // Pause the video if not
      videoElement.pause();
    }
    });
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Zoom the video

This is going to be a CSS-only solution: super fast and jank-free.

What we're going to do is to use sticky positioning on the video container element, and it with in an element which is taller than the viewport height.

If we make the wrapper twice the viewport height, you will have to scroll two 'pages worth' to get past the video.

So the wrapper will have the following style:

.observed-wrapper {
  height: 200vh; /* 200% viewport height */
}
Enter fullscreen mode Exit fullscreen mode

Then we're going add some rules for the video container, and override the margin when the video container element has the 'in-view' class:

.observed {
  position: sticky;
  top: 0px;
  height: 100vh;

  margin-left: 5rem;
  margin-right: 5rem;

  transition: all 0.2s; /* Use CSS transitions to animate */
}

/* When video is 'in view' */
.observed.in-view {
  margin: 0; /* Remove the side margins */
}
Enter fullscreen mode Exit fullscreen mode

And a similar deal for the video element:

video {
    position: absolute;
    top: 3em; /* top spacing */
    left: 0;
    width: 100%;
    height: calc(100% - 6em); /* bottom spacing */
    object-fit: cover;
    transition: all 0.2s; /* Use CSS transitions to animate */
}

/* When video is 'in view' */
.observed.in-view video {
    /* No top spacing, and fill viewport */
    top: 0; 
    height: 100%;
}

Enter fullscreen mode Exit fullscreen mode

Here's how that looks in action:

And there you go: fast, jank-free, with butter-smooth transition animations.

You can play with the real thing on the Wayve homepage.

Top comments (0)