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:
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
};
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);
});
};
Next we are going to create an intersection observer with those options and that callback action:
const observer = new IntersectionObserver(callback, options);
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'));
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();
}
});
};
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 */
}
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 */
}
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%;
}
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)