If you’re a fan of web development, chances are you’ve dabbled with web animations at some point, whether through JavaScript or CSS. Animations are not only fun to create but also add that extra layer of polish to websites, making them more interactive and engaging.
In this article, we’ll take a deep dive into three key tools — requestAnimationFrame, linear interpolation, and CSS transitions— that can help you elevate your animation game and create smooth, performant effects.
If you're not a big fan of moving stuff around frequently on a web page, you probably may have seen this method somewhere before now, but then, it turned out to be one of those unnecessary things in JavaScript that you don't need to clog your head with. I initially had the same thought as well until I took a deep dive.
Keeping things simple, you can think of window.requestAnimationFrame
as a setInterval method but specially optimized for your computer/smartphone and when called, runs multiple times within one second. Now let me explain further...
Let's say you have a function that you wish to call repeatedly every second. That's easy; we'll just use a normal setInterval
. This (setInterval) will call the function every second, infinitely till the tab/browser window is closed.
In order to understand properly what requestAnimationFrame does, let's imagine we have a div on our page and we wish to move this div to the right by 500px; in that case, we'll simply write a function that adds transform: translateX(500px)
to the style property of the div, and then call the function once. Easy right?
Yeah, but it could be better, let's animate the movement of our div from A to point B by gradually moving the div to the right until it gets to its final point. We can achieve this by updating the translateX by a few pixels at an interval until it gets to its final destination using setInterval.
const ourDiv = document.querySelector("div");
let position = 0;
const myFunction = () => {
position += 1;
ourDiv.style.transform = `translateX(${position}px)`;
if (position >= 500) {
position = 0;
}
};
const interval = setInterval(() => {
myFunction();
}, 100);
If you wish to see this animation live, here's a codepen link.
One thing you should take note of here is the interval we used and also the value by which we increment the position each time our function runs. The smaller the value of the interval, the smoother our animation gets, and the larger the value of our increment, the faster the animation goes.
In this case, we're updating the position of our div by 1px 10 times in one second (100ms interval), hence creating an animated visual of the div from point A to point B, and I could say that this is basically how videos work. Think of a video as a collection of images/snapshots taken at intervals and then played in a sequence; just like flipbook animations.
Now, if you checked the codepen I shared, you'll notice that it appears to be jumpy. This is because of the rate at which the frames are updated. In videography, this phenomenon is known as frame rate or FPS (frames per second). It is for this same reason that when buying a new iPhone or a Camera, you see specs like 'can shoot at 30FPS or supports HD video at 60FPS.' What this basically means is, when taking a video with the camera, it captures 60 frames/shots/pictures per second and then plays these frames in a sequence to produce a video.
As a result, cameras with higher FPS produce better quality videos because no frames are skipped and they capture every moment, hence producing a smooth, less jumpy video.
When animating, frame rate—measured in frames per second (FPS)—is critical. In videography, this is similar to how many frames are captured and displayed each second, which affects how smooth the animation appears. Higher frame rates produce smoother animations, but pushing the frame rate too high can negatively affect performance.
Putting all these together, we need to make our animated div less jumpy, and we can achieve this by increasing the FPS. Our current frame rate from the snippet above is about 10FPS which simply means we are updating our div 10 times per second. If we reduce the interval to 50ms, we'll have a frame rate of 20FPS which will give us a smoother animation.
Knowing that a higher frame rate gives us better animations, we could just decide to be clever and use an interval of say, 5ms. This will give us an FPS of 200, and then our animation will be smoother and faster (a little too fast actually), but for a number of reasons that I won't be covering in this article, it takes a toll on the performance of our web page.
Imagine calling a function 200 times in one second, a little too much right? This is where window.requestAnimationFrame comes in!
The window.requestAnimationFrame()
method works just like our setInterval function up there but the difference is that it calls our animation function with a frame rate that matches the device monitor/screen specifications. Most monitors have a refresh rate of 60Hz, some high-quality ones have over 100Hz, and some even have up to 240Hz.
A monitor's refresh rate is simply the maximum of times in one second an object can get updated on screen. So what requestAnimationFrame does is it updates our animation in a rate per second that matches the refresh rate of whatever monitor we're viewing the webpage on.
This gives us the smoothest animation possible on that monitor while maintaining maximum performance. Also, note that calling window.requestAnimationFrame()
executes the function you put in it only once, so we'll have to create a loop in order to continuously update our animation by adding another requestAnimationFrame()
callback inside our function.
const ourDiv = document.querySelector("div");
let position = 0;
const myFunction = () => {
position += 1;
ourDiv.style.transform = `translateX(${position}px)`;
if (position >= 500) {
position = 0;
}
window.requestAnimationFrame(myFunction);
};
window.requestAnimationFrame(myFunction);
In this example, the requestAnimationFrame
function updates the position of a div
, moving it across the screen. It does so at a refresh rate matching the display’s capabilities, providing smoother animation compared to using setInterval
.
With requestAnimationFrame
, you don’t need to manually control the FPS. The function automatically syncs the updates to the display's refresh rate, whether it’s 60Hz or even higher, ensuring both smoothness and efficiency.
Linear interpolation (LERP) is a mathematical function that allows for smooth transitions between two values. It accepts three arguments - start
, end
and percentage
, and then returns a point between the start and end values based on the percentage. I'll explain how this is useful in web animations but first, here's a basic example of a LERP function:
const lerp = (start, end, percentage) => {
const difference = end - start;
return start + difference * percentage;
};
If we call the function above with the following arguments: lerp(0, 100, 0.5)
it will return a point that is 50% in between 0 and 100 which is obviously 50. Now what does this have to do with web animations?
In our example animation up there, where we demonstrated requestAnimationFrame
; we can now do something different. Instead of increasing the position of our div by a constant 1px each time our function gets called to produce a linear animation, using a LERP function to interpolate between the current div position and the final position will produce a very smooth easing visual that slows our div down as it draws closer to its final destination.
Take for example, instead of using position += 1
in our animation function above (the one we used requestAnimationFrame), we instead decided to use position += lerp(position, 500, 0.1)
. The value of position will be equal to 50 when our function runs for the first time; then the next time our function runs the value of position, it will be smaller than 50 because the distance between the starting point (0) and the ending point (500) has changed by 10%.
Our new starting point is now 50 while our ending point remains the same (500), so the next time our function runs, our div will be moved by 10% of [500 - 50] which is 45.
const ourDiv = document.querySelector("div");
let position = 0;
const myFunction = () => {
position += lerp(position, 500, 0.1);
ourDiv.style.transform = `translateX(${position}px)`;
if (position >= 500) {
position = 0;
}
window.requestAnimationFrame(myFunction);
};
window.requestAnimationFrame(myFunction);
In this case, while requestAnimationFrame keeps calling our function over and over again, the value by which we move our div to the right keeps getting smaller and smaller hence creating a smooth, eased-out animation.
Basically, the movement becomes progressively slower, creating an easing effect that looks much more natural and fluid.
CSS transitions provide a convenient way to create animations with minimal code. For instance, you can move an element across the screen with just a few lines of CSS:
.box {
transition: transform 2s ease-out;
}
And the JavaScript to trigger the animation:
const moveBox = () => {
const ourDiv = document.querySelector(".box");
ourDiv.style.transform = `translateX(500px)`;
};
moveBox();
And that’s it! No linear interpolation or window.requestAnimationFrame
needed. Now, you may be thinking - why not just do this from the start then?
Well, it is important to note that while CSS transitions are easy to implement, they lack the flexibility of requestAnimationFrame
paired with LERP for more complex or dynamic animations. For example, with CSS transitions, the animation speed is tied to the duration specified, whereas LERP adapts to the screen’s refresh rate, offering more fluidity.
When building animations for the web, requestAnimationFrame
, linear interpolation, and CSS transitions all offer distinct advantages depending on the use case. Combining these techniques allows for the creation of smooth, performant animations that are optimized for the user’s device. By understanding these tools, developers can achieve polished animation effects, whether for simple UI transitions or complex, dynamic animations.