Using Svelte to create a scroll video effect
Koen van Gilst / November 18, 2020
6 min read • ––– views
In this tutorial, I’ll show you how Svelte’s bind command can be used to create a cool scroll video effect with very little code. The effect we’ll be creating plays a video when the user scrolls up or down. Below is a gif of how this looks on the NYTimes website
Note: The effect itself is a lot smoother than the gifs 😂. Another example can be found here). The effect is also sometimes used on product landing pages. Here's an example on Apple's page for the iMac Pro.
It gives web pages a very dynamic look and I was surprised to learn how easily you can accomplish this with svelte.
Setting up svelte
Following the instructions on the svelte.dev homepage we run the following commands to get started with svelte:
npx degit sveltejs/template scroll-video
npm install
npm run dev
A new svelte project is now running on http://localhost:5000
We start with an empty App.svelte
file, so let’s delete all the code in that file first.
Scrolling formula
When the user scrolls down the page, we want the video the play forward. To accomplish this we'll set the current video position using the following formula:
currentTime = duration * scrollY / total scroll height
where:
- currentTime = the play position in the video
- duration = total playing time of the video
- scrollY = the current vertical scroll position
Binding variables
Now that we figured out what the formula is going to be, let’s see how we can read these values by using svelte’s variable binding.
We can get the vertical scrolling position with the following code:
<script>
let scrollY;
</script>
<svelte:window bind:scrollY />
Here we are binding the vertical scroll position of the window to a variable scrollY. Before we can continue, we have to make sure our window is high enough to allow for scrolling. Let’s add a div
and some CSS for that:
<script>
let scrollY;
</script>
<svelte:window bind:scrollY />
<div class="scroll-container" />
<style>
.scroll-container {
height: 5000px;
}
</style>
Notice how you can combine HTML, CSS and JavaScript within a Svelte component. Almost like you would in a regular HTML file!
To verify if this works you might be tempted to add a console.log just below the line let scrollY;
. If you try that however you’ll see only one 0
being logged. The reason for this is that any code inside the <script>
will run only once (when initializing the component). If you want to keep executing the console.log as the scroll values update, make it reactive using the following notation:
$: console.log(scrollY);
This may look a bit funky (and it is! I’m not even sure if it’s still JavaScript) but you’ll get used to it.
After adding the console.log with the dollar sign, you should see the scroll position logged on every update. Pretty cool for just 2 lines of code!
Similarly, we can use bind on <video>
to get the other values:
<script>
let scrollY;
let time;
let duration;
</script>
<svelte:window bind:scrollY />
<div class="scroll-container" />
<video
bind:currentTime="{time}"
bind:duration
preload="metadata"
muted
src="https://static01.nyt.com/newsgraphics/2019/10/23/turkey-syria-video-upload/71ab097907156ca46fb7ffd4d21dfbd119fb47e8/syria-turkey-reconstruct-7-800.mp4"
type="video/mp4"
/>
<style>
.scroll-container {
height: 5000px;
}
</style>
The cool thing about these bindings is that they are bidirectional. This means that the time
variable not only holds the current playback time but that if we change it, we’re also updating the current playback position of the video! This is also known as two-way data binding which was initially popularized by the original AngularJS.
In the next section, we'll combine this with the formula we made earlier.
Updating video playback on scroll
Before we can continue we first have to make sure the video is always displayed fullscreen in the background while we are scrolling. Let's add a container div and some CSS to accomplish that:
<script>
let scrollY;
let time;
let duration;
</script>
<svelte:window bind:scrollY />
<div class="scroll-container" />
<div class="video-container">
<video
bind:currentTime="{time}"
bind:duration
preload="metadata"
muted
src="https://int.nyt.com/newsgraphics/2020/beirut-explosion-video/main/warehouse-800.mp4"
type="video/mp4"
/>
</div>
<style>
.video-container {
position: absolute;
top: 0;
bottom: 0;
overflow: hidden;
}
.video-container video {
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.scroll-container {
height: 5000px;
}
</style>
Any changes in the scroll position should immediately be reflected in a change in the playback position. In other words: the changes in the time variable should react to the changes in the scroll position. To accomplish this we have to make sure our formula code is reactive. Just as with the console.log we use the $:
notation to do this:
<script>
let time = 0;
let duration;
let scrollY = 0;
$: {
const totalScroll =
document.documentElement.scrollHeight - window.innerHeight;
time = duration * (scrollY / totalScroll);
}
</script>
Result
After adding the last piece of code the video scroll effect should work. To make the scroll effect more visible I've also added a heading and some content blocks. You can see the complete feature in the Svelte REPL below.
Notes on performance
The NYTimes is using optimized, small-size videos on their pages. And with good reason: I've been doing experiments with larger (high definition) videos and it's impossible to get the performance right on all browsers: Safari performs surprisingly well, but Firefox and Chrome quickly start to stutter when the video gets larger.
Apple is using a different technique to accomplish the effect on their pages: They've split up the video into separate frames (images) and they show a different frame when the scroll position changes. This technique performs surprisingly well, even for high-definition video content. It does, however, come at a cost: All those frames have to be downloaded individually, adding up to huge download sizes. This is costly not just for mobile end users, but also if you're hosting them.
If you want to use this technique and keep things simple I suggest using a small and optimized video just like the NYTimes does.
For anyone interested in the performance aspects of this effect: I've tweeted about this here (with a demo site): https://twitter.com/vnglst/status/1307681819635183616