A film role of showing a blobby person being animated running

Intro to RequestAnimationFrame

22 December 2021

requestAnimationFrame is the way to create complex animations with JavaScript. If you’re using an animation library, the chances are that it uses requestAnimationFrame under the hood.

How do you use requestAnimationFrame? We'll start by looking at a simple example to make a line to repeatedly grow:

    <-- HTML -->
    <div id="line"></div>
    /* CSS */
    #line{
    background-color:black;
    height:1px;
    }
    /* JS */
    let width = 0;
    const line = document.getElementById("line");
    let lasttime;
    function step(time) {
      if (lasttime == undefined) {
        lasttime = time;
      }
      let delta = time - lasttime;
      if (width >= 1000) {
        width = 0;
      } else {
        width = width += delta/2;
      }
     line.style.width = `${width}px`;
      lasttime = time;
      window.requestAnimationFrame(step);
    }
    window.requestAnimationFrame(step);

Here’s a Codepen with this code.

There's a lot going on here for such a small animation. 18 lines of pretty weird looking code to make a line get longer.

This is one of the reasons why you might prefer to use CSS for simpler animations. You can actually do the exact same animation with only CSS:

    /* CSS */
    #line{
      height:1px;
      margin-top:10px;
      background-color: black;
      animation-name: line;
      animation-duration: 4s;
      animation-iteration-count: infinite;
      animation-timing-function:linear;
    }
    @keyframes line {
      from {width:0%;}
      to {width:100%;}
    }

And with even less CSS using the animation shorthand property:

    /* CSS */
    #line {
      height:1px;
      margin-top:10px;
      background-color: black;
      animation: line 4s infinite linear;
    }
    @keyframes line {
      from {width:0%;}
      to {width:100%;}
    }

So, I guess the first rule of requestAnimationFrame is ask yourself whether you actually need requestAnimationFrame.

For little animations like this, probably not. But as animations get more complex and interactive, you'll find CSS to be increasingly insufficient. If you need fine-grained control of element positions and timing functions then requestAnimationFrame is the way to go.

To use it, we call the window.requestAnimationFrame function and pass in a callback. Let’s pass in a function that prints “Hello from frame”:

window.requestAnimationFrame(()=>{ console.log("Hello from frame") });

Running this should print “Hello from frame” once. This isn’t particularly exciting. However, what's interesting about it is when the callback function gets run. It looks like it runs immediately, but it actually gets run before the next browser repaint. The browser will usually repaint the page about 60 times a second which is why the callback appears to run instantly.

I'll even prove that it’s not run immediately. When our callback gets called, the browser passes it a decimal number as an argument. This number is the number of milliseconds since the "time origin" of the page (essentially since the page finished loading). We can compare this to the output of performance.now() which prints out the same measurement:

    /* JS */
    console.log(performance.now());
    window.requestAnimationFrame((timestamp)=>{console.log(timestamp)});

When I ran this in codepen it printed off:

    1661.5999999940395
    1680.269

It will print something different every time, depending on browser performance (and a whole bunch of other factors) but there should be a gap of about 30ms between the performance.now() call and the requestAnimationFrame call.

To use window.requestAnimationFrame for animation we can't just throw a callback into it once and leave it. Instead, we want to continously update the page. To get our code to run continuously, we call requestAnimationFrame, passing it our callback function inside the callback. We also need to name our function to do this. We’ll name it step:

    /* JS */
    function step(timestamp){
      console.log(timestamp);
      window.requestAnimationFrame(step);
    }
    window.requestAnimationFrame(step);

This code will continually print timestamps in the console before each browser repaint. But I still wouldn’t exactly call this animation. To actually animate something, we need to change an element’s style on each tick.

Let’s add a centered red box to the page:

    <-- HTML -->
    <div id="growing-box"></div>
    /* CSS */
    body{
      height:100vh;
      margin:0;
      display:flex;
      justify-content:center;
      align-items:center;
    }
    #growing-box{
      width:40vmin;
      height:40vmin;
      background-color:#af0000 ;
    }

Now let’s use requestAnimationFrame to make the box slowly grow. The best way to do this is using the transform CSS property, and the scale function, as this gets GPU accelerated by the browser. So, we grab the box , set a scale variable to start with a value of 1, add 0.01 to the scale on each iteration, and apply the scale to the element on each iteration:

    /* JS */
    let square = document.getElementById("growing-box");
    let currentScale = 1;
    function step(timestamp){
      currentScale += 0.01;

      square.style.transform = `scale(${currentScale})`

      window.requestAnimationFrame(step);
    }
    window.requestAnimationFrame(step);

Here’s a codepen showing what this looks like.

Now we have a growing square, but there’s a couple of problems.

First, it will grow forever. We need to add some kind of check to make sure that it stops (or repeats) at some point.

Let’s add a check which will test whether the scale is equal or above 2, and, if so, reset it to 1.

Second, it will grow unevenly. This is because the interval between browser repaints varies, but we’re changing the scale the same amount each time. It won’t be very noticeable on faster devices, but will become a problem as repaints get more intermittent.

Let’s measure the time between the last frame and the current frame (we’ll use the convention of naming this delta). We use the delta to figure out how far we need to grow the square. We’ll divide delta by 30, just to make sure the square isn’t growing too fast.

    /* JS */
    let square = document.getElementById("growing-box");
    let currentScale = 1;
    let lastTime;

    function step(timestamp){

      if(lastTime==undefined){
        lastTime = timestamp;
      }

      let delta = timestamp - lastTime;
      currentScale += 0.01 * (delta / 30);

      if(currentScale>=2){
        currentScale= 1;
      }

      square.style.transform = `scale(${currentScale})`

      window.requestAnimationFrame(step);
      lastTime= timestamp;
    }
    window.requestAnimationFrame(step);

Now we have a box which repeatedly grows then jumps back down to being small again.

We can still do better.

At the moment, there isn’t any easy way to change the speed of the animation, how big the square gets, or whether the animation loops. So let’s make some variables.

    let animationLength = 1;
    let animationLengthMillis = animationLength *1000;
    let timeThrough = 0;
    let startScale = 1;
    let endScale = 2;
    let looping = true;

We’re storing the animation length (in both seconds and milliseconds), the amount of time that has passed through the current iteration, the start/end points for our scale property, and finally whether the animation should loop.

We need to change a couple things to use these. Firstly, we can use the timeThrough variable to keep track of how far through the animation we are, and add our delta variable to this on each tick.

Then we can get a fraction value of how far through the animation we are by dividing the timeThrough by the animationLengthMillis. Then we set the currentScale to the startScale plus the difference between endScale and startScale multiplied by the fraction through the animation we are:

    let percentThrough = timeThrough / animationLengthMillis;
    let currentScale = startScale + (endScale - startScale) * percentThrough;

This is a lot. But basically what it’s doing is setting the scale based on where we are in the animation.

Now we need to make sure the animation lasts the correct length. So, we check to see if the time that has passed is more than or equal the animation length. If so, we set timeThrough back to zero or return to end the animation (depending on whether we're looping or not).

Here’s how it looks:

    /* JS */
    let square = document.getElementById("growing-box");
    let animationLength = 1;
    let animationLengthMillis = animationLength * 1000;

    let lastTime;
    let timeThrough = 0;
    let startScale = 1;
    let endScale = 2;
    let looping = true;

    function step(timestamp) {

      if (lastTime === undefined) {
        lastTime = timestamp;
      }
      let delta = timestamp - lastTime;

      timeThrough += delta;

      if (timeThrough >= animationLengthMillis) {
        if (looping) {
          timeThrough = 0;
        } else {
          return;
        }
      }

      let percentThrough = timeThrough / animationLengthMillis;
      let currentScale = startScale + (endScale - startScale) * percentThrough;

      square.style.transform = `scale(${currentScale})`;

      window.requestAnimationFrame(step);
      lastTime = timestamp;
    }
    window.requestAnimationFrame(step);

And, here’s the codepen.

You can see how you can edit these variables to your taste to make the animation look the way you need.

Animation can make user interfaces less jarring, and more human-friendly. They’re a necessary tool to have in your user-interface toolbox. For something a bit less trivial you can check out a minimal notetaking app with some squishy animations over at this codepen, and for more information on requestAnimationFrame you can’t go wrong with the MDN web docs.

Back to blog