CSS Animations vs the Web Animations API: A Case Study

Last week, I wrote about how I created the bitsofcode logo animation with CSS. After that, it was suggested that I attempt a comparison between a CSS animation and the Web Animations API, so here it is!

Introduction to the Web Animations API #

As with last week, I'll start this off with an introduction to the Web Animations API. The Web Animations API provides a way for developers to directly manipulate the browser's animation engine using JavaScript.

Creating an Animation #

To create an animation using the Web Animations API, we use the Element.animate() function, which takes two arguments; keyframes and options.

element.animate(keyframes, options);

keyframes #

The keyframes object represents the timeline of events in the animation. There are two ways to write this object. To illustrate them, let's use this grow animation, which will scale an element to twice it's size. Here is the animation using CSS @keyframes:

@keyframes grow {
0% {
transform: none;
}
100% {
transform: scale(2);
}
}

The first way to write the keyframes is to pass a single object. Each key in the object represents the CSS property we want to animate. The value for each key is an array of CSS values we want to animate through. Each value in the array represents a point in the animation timeline.

const growKeyframes = {
transform: ['none', 'scale(2)']
}

The second way to write the keyframes is to write it as an array. Each item in the array represents a point in the timeline, in which we specify each CSS property and value to be applied for that point.

const growKeyframes = [
{ transform: 'none' },
{ transform: 'scale(2)' }
]

By default, each point on the timeline is equally spaced. For example, if we had 5 points on the timeline, the animation transition between each point will be equally 20% of the animation duration.

If we want to alter timing, we can use the offset property with the second format of writing the keyframes. This property accepts a number between 0 and 1, representing the point at which that animation should be. Take, for example, this CSS animation -

@keyframes alteredGrow {
0% { transform: none; }
10% { transform: scale(1.5) }
30% { transform: scale(1.9) }
100% { transform: scale(2) }
}

To account for the uneven spacing in the timeline, we could write it the following way -

const alteredGrowKeyframes = [
{ transform: 'none' },
{ transform: 'scale(1.5)', offset: 0.1 }
{ transform: 'scale(1.9)', offset: 0.3 }
{ transform: 'scale(2)' }
]

options #

The second argument we pass to the animate() function is an object with some options. This object allows us to specify all the properties we would apply to the CSS property being animated if we were using CSS animations. There are 9 options we can specify:

Option Description
id A unique reference for the animation
delay Specifies a delay before the animation begins. Corresponds to the animation-delay CSS property
duration Specifies the amount of time it should take for one cycle. Corresponds to the animation-duration CSS property
iterations Specifies how many times to run a cycle of the animation. Corresponds to the animation-iteration-count CSS property
direction Specifies in which direction to run through the animation timeline. Corresponds to the animation-direction CSS property
easing Specifies how the animation transitions between steps. Corresponds to the animation-timing-function CSS property
fill Specifies the values applied to the element and the start and end of the animation. Corresponds to the animation-fill-mode CSS property
endDelay Specifies an amount of time to delay after the end of the animation
iterationStart Specifies the point n the iteration the animation should start

For example, let's take the alteredGrow animation. With CSS animations, we can apply the animation for 3 seconds, on an infinite loop, alternating directions, after a delay of 2 seconds, with these declarations -

.animated-element {
animation-name: alteredGrow;
animation-duration: 3s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-delay: 2s;
}

Using the Web Animations API, we can do the same with the following options -

const alteredGrowOptions = {
duration: 3000,
iterations: Infinity,
direction: 'alternate',
delay: 2000
}

Using an Animation #

An animation is applied to an element by calling the animate() function on the element and passing the keyframes and options arguments.

const element = document.querySelector('.animated-element');
element.animate(alteredGrowKeyframes, alteredGrowOptions);

Once that function is called, the animation automatically starts playing. However, we can start and stop it using the play() and pause() methods.

const element = document.querySelector('.animated-element');
const myAnimation = element.animate(alteredGrowKeyframes, alteredGrowOptions);

myAnimation.pause();
myAnimation.play();

Support #

Can I Use web-animation? Data on support for the web-animation feature across the major browsers from caniuse.com.

As with my CSS animation, I recreated a short section of the full animation that was created. Here's a comparison between all three versions -

Creating a Timeline #

To recap, here are the animation steps for the left section of the logo (the letters "bitso", with the opening o).

  1. Move left
  2. Return to middle
  3. Stay in middle (while waiting for the right section to move right)
  4. Move left
  5. Rotate
  6. Slowly increase rotation
  7. Return to unrotated position
  8. Return to middle

Based on these steps, this is the timeline I mapped out for the same left section -

Timeline

Based on this timeline, here is the keyframes object for the Web Animation, with the styling for each step in the timeline.

const logoSectionLeftKeyframes = [
{ transform: 'none' },
{ offset: 0.125, transform: 'translateX(-15px)' },
{ offset: 0.25, transform: 'none' },
{ offset: 0.5, transform: 'none' },
{ offset: 0.625, transform: 'translateX(-15px)' },
{ offset: 0.67, transform: 'translateX(-15px) rotate(-10deg)' },
{ offset: 0.72, transform: 'translateX(-15px) rotate(-10deg)' },
{ offset: 0.82, transform: 'translateX(-15px) rotate(-15deg)' },
{ offset: 0.875, transform: 'translateX(-15px)' },
{ transform: 'none' }
];

Since I needed to use the offset property, I decided to use the array format for the keyframes.

Setting the Options #

The options I set for each section were simple; each cycle of the animation should run for 3 secons, looping infinitely.

const logoSectionOptions = {
duration: 3000,
iterations: Infinity
};

Applying the Animation #

Applying the animation using the Web Animation API was a lot more fiddly than with CSS. This is because I only wanted the animation to run if the logo was being hovered over or in focus. As I mentioned, by default, web animations run as soon as they are created.

To work around this, I had to first create the animation, immediately pause it, then add eventListeners for when I wanted the animation to play or pause. Additionally, since I had to apply an individual animation to each letter, I had to work with several animations at once. Here is how I executed it -

// Array to store all animations
const animations = [];

function playLogoAnimation() {
animations.map((animation) => animation.play())
}

function pauseLogoAnimation() {
animations.map((animation) => {
animation.pause();
animation.currentTime = 0; // Reset animation to start state
})
}

function createLogoAnimation() {
const logoSectionLeftEls = Array.from( document.querySelectorAll('.logo-section-left') );
logoSectionLeftEls.forEach((el) => animations.push(el.animate(logoSectionLeftKeyframes, logoSectionTiming)))

// Animation for middle and right sections here …

// Immediately pause animation once created
pauseLogoAnimation();
}

createLogoAnimation();

// Event listeners to play or pause animation
const siteTitleLink = document.querySelector('.site__title a');
siteTitleLink.addEventListener('mouseover', playLogoAnimation);
siteTitleLink.addEventListener('mouseout', pauseLogoAnimation);
siteTitleLink.addEventListener('keyup', (e) => {
if ( e.keyCode === 9 ) playLogoAnimation();
});
siteTitleLink.addEventListener('keydown', (e) => {
if ( e.keyCode === 9 ) pauseLogoAnimation();
});

Here's the CodePen with the full animation:

See the Pen MmJOzR by Ire Aderinokun (@ire) on CodePen.

CSS Animations vs the Web Animation API #

As with everything, whether to use CSS or JavaScript animations, depends very much on the particulars of the animation. As a general rule, CSS animations should be used for small, UI-related animations such as showing a tooltip. The Web Animation API should be reserved for more advanced effects that need fine-tuned control. That said, here was my comparison between the two methods for this animation.

Performance #

The performance of CSS vs JavaScript animations can vary a lot depending on which properties we are animating. Generally, it is advised to only animate the transform and/or opacity properties, as these animations can be executed on a different thread from the browser's main thread.

Changing `transform` does not trigger any geometry changes or painting, which is very good. This means that the operation can likely be carried out by the compositor thread with the help of the GPU.

CSS Triggers

Since my animations only used the transform property, I was not able to see any significant difference in performance between the two methods. Using Firefox's DevTools, I measured the frame rate of both animations and got the exact same rate of 60 FPS, even with Off Main Thread Animation enabled.

I wasn't able to find more ways to measure the performance between the two versions. If you know of any better ways, do leave a comment below.

Developer Experience #

In this case, I personally found the CSS animation easier to work with than the Web Animation API, mainly because of the extra work it took to get the animation to be played/paused using the latter. If I were doing a more complex animation, for example for a game, the Web Animation API would definitely be the way to go. However, for this case, I think the CSS animation was simpler to implement.

Keep in touch KeepinTouch

Subscribe to my Newsletter 📥

Receive quality articles and other exclusive content from myself. You’ll never receive any spam and can always unsubscribe easily.

Elsewhere 🌐