Meet the new Web Animations API

While browser support for the Web Animations API (WAAPI) may look rather bleak at the moment, when the other browsers catch up, it will be the sharpest and fastest way to animate anything without resorting to WebGL, since it enables us to programmatically create CSS3 animations, which are subject to a very healthy helping of browser optimisation.

But WAAPI is much, much more than that. It’s not just an opportunity to throw out all your animation libraries like GSAP, Velocity.js, jQuery, etc. CSS3 Animations already gave you that opportunity for basic things like fading content in and out. WAAPI enables you to play, pause, rewind and cancel animations as though they were videos. You can even alter the speed at which the animation is playing. The potential is absolutely enormous.

So let’s get our teeth into the thing.

Recap: CSS Animations

I think it’s best to approach WAAPI with at least a little familiarity with CSS animations. The syntax looks something like this:

@keyframes my-animation { 
from, 0% { opacity: 0; visibility: hidden; }
to, 100% { opacity: 1; visibility: visible; }
}
.my-element {
animation-name: my-animation;
animation-timing-function: cubic-bezier(0.4,0,0.2,1);
animation-duration: 1s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-direction: normal;
animation-delay: 2s;
animation-play-state: initial;
}

So, let’s look at this bit by bit. The @keyframes statement is the definition of your animation. You use this to say precisely what will happen across the time specified in your element's animation-duration property. When you have two states, you can use from and to, but when you have multiple keyframes you need to specify a percentage value, representing at what percentage of the total duration that keyframe should fire.

The rest is pretty self-explanatory. The animation-name is required and must match a set of keyframes you specified. The animation-timing-function works exactly as it does for CSS transitions, and includes the usual predefined defaults like linear, ease, and so on. You can also define your own bezier curves, which I prefer to do because I like the 'fast in, slow out' curve I've given as an example above.

animation-play-state is a new property. CSS Animations have been available for developers to use for a long time now, with even browsers as old as Safari 5.1 supporting a prefixed version of the above syntax. animation-play-state is something new, which seems to have arrived in Chrome since the last beta update (I just updated to Chrome 54 and I have only just seen this property become available). This is of course landing as a result of the Web Animation API's play/pause functionality being realised, because you might as well provide access to that functionality through CSS so that animations can be paused and played using :hover or input:checked + .animated-element and so on.

animation-iteration-count is important: you can create an everlasting animation by setting the iterations to be infinite, and we'll be using that later when we build a demo of WAAPI.

So, with the code above, you could quite easily make a simple blinker. Lovely. But what if you wanted to make something like a carousel, or a scrolling marquee style product showcase? Now that CSS has animation-play-state, it's quite easy to do with even the CSS syntax, but you'll need to know:

  • how many elements will be in the carousel
  • how big they are going to be, expressed as percentages or precise pixel counts

and you’ll also need all of the elements to be the same width. It seems like quite a lot, but actually it’s fairly reasonable to expect that your products will be displayed as squares of equal width, and that your back-end is setup to display, say, 12 products with a ‘view more’ button nearby.

If, however, you really did get such a situation as my imaginary scrolling marquee/carousel featuring, say, photographs of different widths, how would you use animations then?

Enter WAAPI

Let’s imagine our gallery. Our end result will look like this:

I’ve used CSS to create that marquee, which is easy enough with a known number of equal-width elements. 8 elements, 25% each, so that’s a translation in X of -100%. Let’s have a look at how to achieve this for an unknown number of random-width elements:

The CSS for that animated marquee looks like this:

@keyframes marquee1 { 
from, 0% { transform: translate3d(0,0,0); }
to, 100% { transform: translate3d(-100%,0,0); }
}
.marquee {
display: block;
white-space: nowrap;
padding-top: 16px;
font-size: 0;
animation-name: marquee1;
animation-timing-function: linear;
animation-duration: 8s;
animation-iteration-count: infinite;
animation-fill-mode: forwards;
animation-direction: alternate;
}
.marquee:hover {
animation-play-state:paused;
}
.marquee .cell {
position: relative;
display: inline-block;
font-size: 16px;
width: 25%;
padding: 8px;
height: 100px;
}
.marquee .cell .content {
position: absolute;
top: 8px;
left: 8px;
bottom: 8px;
right: 8px;
padding: 8px;
color: #fff;
background: crimson;
}

The code we’ll use for our JavaScript WAAPI example will look like this:

The HTML:

<div class="grid">
<div class="cell w33">
<div class="content"> 1 </div>
</div>
<div class="cell w66">
<div class="content"> 2 </div>
</div>
<div class="cell w100">
<div class="content"> 3 </div>
</div>
<div class="cell w83">
<div class="content"> 4 </div>
</div>
<div class="cell w16">
<div class="content"> 5 </div>
</div>
<div class="cell w25">
<div class="content"> 6 </div>
</div>
<div class="cell w25">
<div class="content"> 7 </div>
</div>
<div class="cell w50">
<div class="content"> 8 </div>
</div>
<div class="cell w100">
<div class="content"> 9 </div>
</div>
<div class="cell w75">
<div class="content"> 10 </div>
</div>
<div class="cell w25">
<div class="content"> Finish </div>
</div>
</div>

The CSS:

* { box-sizing: border-box } 
body { 
margin: 0;
overflow: hidden;
background: #08f
}
.grid { font-size: 0 } 
.cell { 
display: inline-block;
font-size: 2rem;
font-weight: 700;
position: relative;
width: 100%;
height: 30vh;
padding: 1rem .5rem;
text-align: center;
cursor: pointer;
}
.cell .content { 
display: block;
position: absolute;
top: .5rem;
bottom: .5rem;
left: .5rem;
right: .5rem;
padding: 1rem;
color: #555;
background: #fff;
}
.cell.w16 { width: calc(100% / 6) } 
.cell.w25 { width: 25% }
.cell.w33 { width: calc(100% / 3) }
.cell.w50 { width: 50% }
.cell.w66 { width: calc(100% / (3 / 2)) }
.cell.w75 { width: 75% }
.cell.w83 { width: calc(100% / (6 / 5)) }
.cell.w100 { width: 100% }

So that’s a grid and 10 grid cells, each serving as containers for an image or something like that. I’m just using numbers for now. To turn this into something which we can scroll horizontally, we need to first set the .grid element's white-space property to nowrap. We'll do this using JavaScript, so that the items appear as a normal grid if JavaScript is turned off or if WAAPI is not supported.

We need to find our grid element and its children, get the combined width of the children, subtract from that the width of the containing element, and then programmatically animate the grid to translate itself by the number of pixels we calculated earlier, before returning to its original position.

If that sounds complicated, it’s not about to get any simpler:

// First, create a variable to be a reference to our animation later, 
// so by using a closure, we can start, stop and cancel the animation. var marquee;
// Then let's grab the element we're going to move around var marquee_el = document.querySelector( '.grid.marquee' );
// and its children, so we know how much we have to move it around by
var children = marquee_el.querySelectorAll( '.cell');
function createMarquee(){ 
/* We're going to recreate the marquee animation when the viewport is resized, so get rid of any existing animation first */
if ( typeof marquee !== 'undefined' ) marquee.cancel();
  // We set this dynamically, so the thing will 
// gracefully degrade to a typical grid of items
marquee_el.style.whiteSpace = 'nowrap';
  // Create a variable for the distance by which 
// the grid element will be transformed
var displacement = 0;
// Add up the width of all the elements in the marquee
for ( var j = 0; j < children.length; ++j ) {
displacement += children[j].clientWidth;
}

/* Crucial: subtract the width of the container; Optional: take the opportunity to round the displacement value down to the nearest pixel. The browser may thank you for this by not blurring the shit out of your text. */
displacement = (displacement - marquee_el.clientWidth) << 0;

/* Now for the juicy part. The WAAPI. By using the variable 'marquee' we created in the parent scope, we can easily use the reference to pause/cancel the animation later */
  marquee = marquee_el.animate([ 
/* element.animate() accepts two arguments: an array of keyframes and a sort of configuration object, much like the CSS syntax.
First are your keyframes: so your 'from' or '0%' keyframe translates to 'offset: 0', '100%' translates to 'offset: 1', and anything in betwen like '54%' will be 'offset: .54'.
One object per keyframe. The ability to define a dynamic translateX value already gives you an idea of why WAAPI is useful: */
{transform:'matrix(1,0.00,0.00,1, 0, 0)', offset: 0},
{transform:'matrix(1,0.00,0.00,1,'+ -displacement +', 0)', offset: 1}
// you don't have to use matrix, I just like it.
],
{ // 1 second for each element in marquee
// Entirely arbitrary decision
duration: children.length * 1e3,

/* Can be 'ease', 'cubic-bezier(.4,0,.2,1)', etc. can also be a stepping function, like 'steps(4)', 'steps(10, end)' see 'MDN: Using the Web Animations API' (https://goo.gl/PtVEkQ) */
easing: 'linear',

// Useful if you don't want the animation to start until your content
// has loaded from, say, a REST API and you want to speculate a
// reasonable time for that to take delay: 0, // Kind of crucial for what we want to make...
// NB: 'Infinity' not 'infinite'
iterations: Infinity,
      // Invert animation after completion, so it scrolls backwards    
direction: 'alternate',
     /* You would use this if your animation is set to occur only a finite number of times, and you wanted the animated element to finish at the end keyframe, rather than the first keyframe */   
fill: 'forwards'
});
}

This might look hideously complicated, but this kind of brevity to unlock the kind of performance and fluidity of animation is unprecedented. With jQuery and $(element).animate(), you’d have plenty of framedropping and jankiness to enjoy. With GSAP/Velocity.js, the story is a good deal better, but they both still represent an additional JavaScript library and HTTP request at the end of the day.

This is just a function.

Now, maybe you don’t think it’s particularly brief/terse. But if I just remove my comments…

function createMarquee(){ 
if (typeof marquee !== 'undefined') marquee.cancel();
var displacement = 0;
marquee_el.style.whiteSpace = 'nowrap';
for (var j = 0; j < children.length; ++j) displacement += children[j].clientWidth;
displacement = (displacement-marquee_el.clientWidth) << 0;
marquee = marquee_el.animate([
{ transform: 'matrix(1,0.00,0.00,1, 0, 0)', offset: 0 },
{ transform: 'matrix(1,0.00,0.00,1,'+-displacement+',0)', offset: 1 }
],{
duration: children.length * 1e3,
easing: 'linear',
delay: 0,
iterations: Infinity,
direction: 'alternate',
fill: 'forwards'
});
}

Yeah, pretty brief. See the Codepen link and demo for a proper demonstration and to actually see the code in a clear way!

(Open in Codepen) · (View live demo)

This still might not seem that great: now that browsers will support the animation-play-state as a CSS property, open to manipulation from hover effects and so on, the value of the WAAPI may be constrained to edge cases. But now at least, when there is an edge case, there is a simple and clean way to approach it, without libraries or horribly inaccurate setTimeouts/Intervals.


Originally published at amdouglas.com.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.