Bringing Pages to Life with the Web Animations API

Dudley Storey
Share

This article is by guest author Dudley Storey. SitePoint guest posts aim to bring you engaging content from prominent writers and speakers of the JavaScript community.

One API to Rule Them All

Animation on the web has long been divided into four disparate camps:

  • CSS transitions and animations are very performant and provide keyframing, but are also time-consuming to build, and provide only basic start-and-end control in CSS and JavaScript. This has tended to relegate them to simple UI response animations, loops, and page load animations.
  • SMIL (Synchronized Multimedia Integration Language) is very powerful, but it’s also syntax-heavy and has incomplete browser support. It’s also limited to controlling elements solely inside the context of SVG.
  • JavaScript offers direct control of elements, but has no understanding of designer-friendly functions like keyframes or easing, and lacks the native optimization and performance of CSS. Canvas API animation is wonderful, but still lacks an understanding of animation fundamentals, and can’t animate arbitrary DOM elements.
  • JavaScript animation frameworks like Greensock attempt to pave over the traditional deficits of JavaScript when it comes to animation, but have all the associated drawbacks of frameworks: page load, performance, and learning a new syntax.

The Web Animations API seeks to integrate the best features of all of these into a single, unified specification, while eliminating the drawbacks, creating a native understanding of keyframes, easing, and element control in JavaScript, with the same on-screen performance as CSS. With core components of the specification now supported in Chrome and Firefox, and development either announced or underway in other browsers, including Safari and Edge, together with the availability of a robust polyfill, it’s time to give the Web Animations API serious consideration for bringing web pages to life.

Keyframes in JavaScript

Let’s take one of the simplest possible examples of a keyframe animation: moving a red ball element from one side of the page to the other. Regardless of which technique we use, the element will be the same:

<div id="redball"></div>

As will the initial CSS:

body {
  margin: 0;
  background: #000;
  overflow: hidden;
  min-height: 100vh;
}

#redball {
  background: red;
  width: 30vmin;
  height: 30vmin;
  border-radius: 50%;
}

I’ve used vmin units so that the element always remains symmetrical while responding to the size of the viewport.

In CSS, moving the ball from one side of the page to the other would require something like the following:

@keyframes moveBall {
  from {
    transform: translateX(-20vw);
  }
  to {
    transform: translateX(100vw);
  }
}

This animation would be called from the declaration for the red ball element:

#redball { 
  animation: moveBall 3s infinite;
}

The result, shown in Codepen:

See the Pen Basic CSS Red Ball Animation by SitePoint (@SitePoint) on CodePen.

There are a few things to note about the animation at this point, including the fact that easing (speeding up at the beginning and slowing down at the end) is automatically built in.

Enter the Web Animations API

Keeping the HTML and initial styles, let’s remove the CSS animation and substitute JavaScript that uses the Web Animation API to accomplish the same thing:

var moveBall = document.getElementById('redball').animate([{
  transform: 'translateX(-20vw)'
}, {
  transform: 'translateX(100vw)'
}], {
  duration: 3000,
  iterations: Infinity,
  easing: 'ease'
});

You can see that the animation takes most of the same syntax as CSS, but represents it as an object with an array representing the keyframes. We don’t have to explicitly declare to or from in the keyframes – JavaScript will automatically distribute the keyframes evenly for us, although declaration is also entirely possible.

The animation won’t play yet; just like in CSS, we have to call it:

moveBall.play();

The keyframe part of the syntax will become even easier in future browsers, as aspects of the CSS transform syntax that were previously values become available as properties:

translateX: -20vw;

This change to the specification is already available in Chrome Canary, but it be a year or two before the new syntax is available in all modern browsers.

There are a few other things to note about the script:

  • JavaScript takes animation timing in milliseconds, rather than the standard CSS seconds (milliseconds is also available in CSS, so long as ms is suffixed to a timing value).
  • The Web Animations API specifies the number of iterations as iterations, rather than the CSS property of interation-count (when it is defined separately); the keyword is Infinity (with a capital I) for the number of repetitions, rather than CSS’s infinite.
  • In the Web Animations API the default easing is linear, so in our demo we specify ease the default value for CSS Animations.

The result is functionally equivalent to the CSS animation.

See the Pen Basic Red Ball Anim in Web Animation API by SitePoint (@SitePoint) on CodePen.

Of course, duplicating a CSS animation like this in JavaScript doesn’t really use the dynamism of the scripting language nearly to its maximum ability; to demonstrate that, we’ll create a much more fully-featured animation.

Dealing Images

Stack of images animated using the Web Animations API

Some time ago I was interesting in creating an animation that dealt a series of images onto the web page, like flicking playing cards onto a table. Writing individual animations for each card in traditional CSS would have taken a great deal of time, and would have always delivered the same result. Instead, I used the Web Animations API as the perfect tool for the job.

Progressive Cards

We want the images to be seen regardless of whether JavaScript or the Web Animations API is enabled or not, so we start by adding a series of images to the page:

<div class="shuffle expose">
  <img src="bridgefog.jpg" alt>
  <img src="daisyface.jpg" alt>
  <img src="drowninghand.jpg" alt>
  <img src="firefigure.jpg" alt>
  <img src="shellhand.jpg" alt>
  <img src="waterfeet.jpg" alt>
</div>

I’ve left the alt values of the images blank to keep the code clear; in the production version, they would be filled with descriptions. The photographs are by .tafo., used under a Creative Commons Attribution-NoDerivs 2.0 Generic license.

We can add a little CSS to bring the images on with animation if the no-JavaScript condition is still true:

@keyframes reveal {
  to {
    opacity: 1;
  }
}
.shuffle {
  min-height: 100vh;
  position: relative;
}
.shuffle img {
  width: 33%;
  opacity: 0;
}
.expose img {
  animation: reveal 1s forwards;
}

The individual image delivery could be delayed with:

.expose img:nth-child(1) { animation-delay: 1s; }
.expose img:nth-child(2) { animation-delay: 2s; }
.expose img:nth-child(3) { animation-delay: 3s; }

Naturally, these declarations could be automated with Sass or another preprocessor.

The images will have a border if the parent element has a class of webanim, which will be applied with JavaScript:

div.webanim img {
  border: 1.4vw solid #eee;
}

The JavaScript

We’ll add the script to the end of the page. I’ll need several random values in the animation, so I’ll create a function to produce those:

function getRandom(min, max) {
  return Math.random() * (max - min) + min;
}

I’ll then gather the image container and all the images inside them, together with setting an incrementor variable:

var imgContainer = document.querySelector(".expose"),
    imgSeq = document.querySelectorAll(".shuffle img"),
    i = 1;

Then, I’ll remove the expose class (leaving the images at an opacity of 0 and unanimated (since the CSS animation only works if the container has an expose class:

imgContainer.classList.remove("expose");
imgContainer.classList.add("webanim");

The webanim class places the images with a border.

The bulk of the script takes place in a forEach loop inside a function. I’ll call the function once I’m assured that all the image data is loaded (as opposed to the image nodes merely appearing in the DOM) using the excellent imagesLoaded script by David DeSandro:

function racknstack() {
  Array.prototype.forEach.call(imgSeq, function(photo) {
    setTimeout(function() {
      photo.style.position = "absolute";
      photo.style.width = getRandom(33, 45) + "%";
      photo.style.left = getRandom(-5, 65) + "%";
      photo.style.top = getRandom(-6, 60) + "vh";
      var animate = photo.animate([{
        opacity: '0',
        transform: 'rotate(' + getRandom(-12, 12) + 'deg) scale(1.2)',
        boxShadow: '0 0 12px 12px rgba(0,0,0,.3)'
      }, {
        opacity: '1',
        transform: 'rotate(' + getRandom(-8, 8) + 'deg)',
        boxShadow: '0 0 6px 6px rgba(0,0,0,.3)'
      }], {
        duration: 2000,
        fill: 'forwards'
      });
    }, 1800 * i)
    i++;
  })
}

As before, we must call the function to start things off:

imagesLoaded(imgSeq, racknstack);

The result:

See the Pen Random Stacked Images w/ WebAnim API & Progressive JS by SitePoint (@SitePoint) on CodePen.

In turn, the function:

  • sets each image to an absolute position;
  • generates a random width for the image (as a percentage, so the image remains fluid and responsive)
  • generates a random position for the image (including a possible negative position, meaning that it may appear slightly off the edge of the viewport)
  • animates the photo using the Web Animation API. The keyframes fade the image in from 0 to solid opacity, while rotating the image

The forwards fill is necessary because we want the images to stay in their end state after the animation has completed.

From Strength to Strength

Development on the Web Animation API continues apace:

Conclusion

As you can see, the Web Animations API allows us to move from the specific, declarative, step-by-step nature of CSS animations to the dynamic, imperative approach of JavaScript that allows for expressive, random, performant animation.

I’d love to see your explorations in the Web Animation API, especially in production sites: let me know in the comments below!