Scroll-based 60 fps animations
Last update on
It's hard to innovate in the web world. There's a combination of well-known patterns and technical limitations that constrain the possibilities. The businesses are afraid of big changes because consolidated UX helps to keep the users engaged. And technical aspects like the form factor, keyboards, mouses, etc. Limit how we interact with digital products.
However, sometimes small yet powerful changes happen. Nothing as disruptive as the introduction of the touch screen. But meaningfully nonetheless. For example, the uber-famous hamburger button. It's not a crazy innovation. Not at all. But it's a great improvement for toggling side menus. Nowadays, it's the de facto solution. All the users over the world understand what this icon means. And even companies like Google included this in their official design guidelines.
Today, we're talking about another of these little giants changes. Scroll-based animations. They're not new; of course not. But now we know how to run smooth 60 fps scroll animations. And these look dope.
You probably have seen them in some product landing pages. Such as the new Airpods Pro from Apple. It's slick, responsive and creates a stunning visual. So, how does this work?
The abstract idea is to animate using a sequence of images in rapid succession. You know, like a flip book!
To be honest, it's not the most obvious solution. You may think about <img />
, some scroll
event hijacking and some offset
calculations. Wrong! If you do it that way everything will end up being clunky and messy. Instead, we're gonna follow these golden rules:
canvas
overimg
. Long story short, thecanvas
can render changes way faster thanimg
. The classic image tag is for static views.Preload the assets. This is mandatory. Otherwise, on every render it'll download the image. So, it'll look clunky.
Do not hijack the
scroll
event. Instead, fix or stick thecanvas
on a long-height component.
And a few drawbacks:
You're gonna be fetching hundreds of images. Consider a good compression and even checking the network first to see if it's worth downloading for your user. To be fair, this technique is how video works; but lighter. For example, the gif above weights almost 5MB and it's highly compressed and low quality. Instead, the whole set of images for the demo weights 8MB but every pic is 2560x1440px in
jpeg
format. So, it's not that bad.The most important bit for this visual to work are the images. You'll need a high-quality sequence of photos before implementing.
With the proper instructions applied you can easily get something like this.
Enough talking. Let's see some code. First, we need a basic markup:
<!-- Preload -->
<head>
<!-- Repeat for as many frames as needed -->
<link
rel="preload"
as="image"
type="image/jpeg"
href="/asset/frame/001.jpeg"
/>
</head>
<!-- Markup -->
<div class="container">
<canvas></canvas>
</div>
And then a bit of JavaScript to handle the render animation.
const context = canvas.getContext('2d')
const image = new Image()
const frameMax = 174
$container.addEventListener('scroll', () => {
const scrollTop = $container.scrollTop
const maxScrollTop = $container.scrollHeight - window.innerHeight
const scrollFraction = scrollTop / maxScrollTop
const current = Math.floor(scrollFraction * frameMax)
const frame = Math.min(frameMax - 1, current)
requestAnimationFrame(() => {
// Update every frame
image.src = createFrameRoute(frame + 1)
context.drawImage(image, 0, 0)
})
})
Notice the requestAnimationFrame
(RAF). This is the key concept on the JavaScript side. It allows the animation to be smooth even when the website main thread is heavy on other stuff.
Finally, we need to add some spicy CSS.
.container {
/* The long-height container to */
height: 600vh;
position: relative;
}
canvas {
max-height: 100vh;
max-width: 100vw;
/* Keep the canvas in front of the container while scrolling */
position: sticky;
top: 0;
}
We're not gonna focus on the details of how sticky
works. For such will be another paper. It'll depend on your layout needs, as well. So the best you can do is try yourself!