Categories:

Web Animation API- Unleashing the Power of CSS keyframes in JavaScript

Posted: Nov 6th, 2017

If you've ever worked with CSS3 key frames animation before, you've probably come to both appreciate and feel severely hampered by the feature. On one hand, CSS keyframes lets you create intricate animations using pure CSS, though therein also lies the problem- everything must be declared upfront inside the CSS. One of my favorite methods in jQuery is the animate() method, which lets me quickly set up animations on elements without switching back and forth between CSS and JavaScript.

Taking a page from jQuery, JavaScript's Web Animation API finally offers an easy way in native JavaScript to animate elements using the full power of CSS keyframes, all without having to leave the comfort of the JavaScript environment. Convenient methods and event handlers lets you pause, rewind, jump to a certain point in the animation timeline, and more.

Ok, to the all important question first- browser compatibility. The core features of Web Animation API is already supported by all major browsers except IE, according to CanIuse:

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

To start using WAAPI today, you can turn to the web-animation API polyfill, which brings IE as well onto the playing field. So no more excuses to not continue reading!

Creating a simple keyframes Web Animation

To animate a keyframes animation using the Web Animation API, just call the animate() function on the element:

Element.animate(keyframes, keyframeOptions)

This function accepts two arguments:

  • keyframes: Array of literals containing a JavaScript representation of the desired CSS keyframes

  • keyframeOptions: A literal containing additional settings for the animation, such as easing, duration, fill-mode etc.

Take a look at the following simple example, which uses the animate() function instead of CSS keyframes to render an animation:

The keyframes argument

The first argument of animate() is an array of literals that each contain one keyframe, which together comprise your desired animation. Here is what I used for the above example:

var boxframes = [
	{
		transform: 'translateX(0)',
		background: 'red',
		borderRadius: 0
	},
	{
		transform: 'translateX(200px) scale(.5)', 
		background: 'orange',
		borderRadius: 0,
		offset: 0.6 /* set explicit point (60%) when frame starts */
	},
	{
		transform: 'translateX(400px)',
		background: 'green',
		borderRadius: '50%'
	}
]

If we were to declare the above using pure CSS, it looks like this:

@keyframes animatethebox{
	0%{
		transform: translateX(0);
		background: red;
		borderRadius: 0;
	}
	
	60%{
		transform: translateX(200px) scale(.5);
		background: orange;
		borderRadius: 0;
	}
	
	100%{
		transform: translateX(400px);
		background: green;
		borderRadius: 50%;
	}
}

As you can see, the two syntax are very similar, and if you're already familiar with CSS keyframes, will have no problems porting it over to JavaScript. A few differences with the JavaScript version worth keeping in mind:

  • In the JavaScript version, property string values should be in quotes (transform: 'translateX(0)')

  • Property names with a hyphen must be converted to camelCase instead (borderRadius: 0).

  • A comma instead of semicolon should trail each property declaration (except the very last property)

By default, keyframes set using JavaScript are evenly spaced when played, with the same amount of time given to each keyframe. However, by adding an offset property inside a keyframe, we can set the point that that keyframe should start playing, such as 0.6 for the 60% mark, similar to that using pure CSS.

The keyframeOptions argument

The second argument for the animate() method is a literal that fine tunes the behavior of the animation. Many of the options are mapped directly from CSS's animation-* properties, such as "animation-delay", "animation-fill-mode" etc. All properties are optional, and fall back to their default values if not set:

 
property CSS Property Equivalent Description
id none Option that sets the name of this Animation to refer back to later in your code.
delay animation-delay The delay (integer) before the animation starts in milliseconds. Defaults to 0s.
direction animation-direction Defines whether the animation should play as normal, in reverse, or alternate between the two. Possible values are:
  • normal: The animation plays forward as normal. After each animation cycle, the animation resets to the beginning state and starts over again. Default value.
  • reverse: Plays the animation in reverse, starting from the end state.  After each animation cycle, the animation resets to the end state and starts over again.
  • alternate: The animation alternates between normal and reverse directions. In reverse, the animation starts at the end state and plays backwards. The animation timing function is also reversed.
  • alternate-reverse: The animation alternates between reverse and normal directions, starting in reverse for the first iteration.
duration animation-delay The duration (integer) of the animation in milliseconds, such as 1000. Default to 0 (no animation, jumps to last frame).
easing animation-timing-function Sets the easing function used to animate the @keyframe animation. Valid values include "ease", "ease-in", "ease-in-out","linear", "frames(integer)" etc. Defaults to "linear".
endDelay n/a The number of milliseconds to delay after the end of an animation. This is useful when sequencing multiple animations based on the end time of another animation. Defaults to 0.
fill animation-fill-mode Defines how the animation should apply styles to its target when the animation isn't playing anymore. Defaults to "none". Possible values are:
  • none: The animation should not apply any styles to the target when it is not playing. Default value.
  • forwards: The target element will retain the computed styles defined in the last key frame (ie: when key frame is at 100%) when the animation isn't playing.
  • backwards: The target element will retain the computed styles defined in the first key frame (ie: when key frame is at 0%) when the animation isn't playing.
  • both: The target element will retain the computed styles defined in both the first and last key frames when the animation isn't playing.
iterationStart n/a Sets the point in the iteration the animation should start. The value should be a positive, floating point number. In an animation with 1 iteration, a iterationStart value of 0.5 starts the animation half way through. In an animation with 2 iterations, a iiterationStart value of 1.5 starts the animation half way through the 2nd iteration etc. Defaults to 0.0.
iterations

 

 

animation-iteration-count Sets the number of times the animation should run before stopping. A value of Infinity means forever. Defaults to 1.

Here is the keyframeOptions argument I used in the example above:

var boxref = document.getElementById("box")
boxref.animate(boxframes, {
	duration: 1000,
	fill: 'forwards',
	easing: 'ease-in'
})

If we wanted to define the same options in CSS using the animation shorthand property, it would look like this:

animation: animatethebox 1s ease-in forwards;

Controlling an Animation (playing, pausing it etc)

Part of the beauty of creating keyframe animations using the Animation API is that the result can be manipulated on demand, such as pausing, skipping forward, or hooking into event handlers of the animation. The first step to doing all of this is to assign the animation to a variable when calling the animate() method:

var myanimation = Element.animate(keyframes, keyframeOptions)

This creates a reference to the Animation object instance, to allow us to manipulate the animation via various exposed properties and methods:

var myanimation = Element.animate(/* .. */)
myanimation.pause() // immediately pause animation to control it manually
myanimation.curentTime = 1000 // jump to 1 second from start of animation
myanimation.play() // play animation

Here's the original example modified to play back using controls:

Notice in this example, I call the animate() immediately on the target element, which should cause the animation to run immediately. To prevent that, I call the pause() method right afterwards. This is the common pattern to use when you wish to control an animation manually:

var boxanimation = boxref.animate(boxframes, {
	duration: 1000,
	fill: 'both',
	easing: 'ease-in'
})

boxanimation.pause()

Animation object Instance Properties and Methods

The following lists the properties, methods, and event handlers of the animation object instance, which as mentioned is created when you assign a reference to the animate() method:

Properties

  • currentTime: Gets or sets the current time value of the animation in milliseconds.
  • effect:  Gets or sets the target effect of an animation. Support for this property is currently limited in all browsers.
  • finished: A promise object that's resolved when the animation has completed. Support for this property is currently limited in all browsers.
  • id: Gets or sets a string used to identify the animation.
  • playbackRate: Integer that gets or sets a playback rate of the animation. For example, 1=normal, 0 = pause, 2 = double, -1 = backwards etc.
  • playState: Read-only property that returns the current state of the animation: "idle", "pending", "running", "paused", or "finished".
  • ready: A promise object that's resolved when the animation is ready to be played. Support for this property is currently limited in all browsers.
  • startTime: Floating point number that gets or sets the current time (in milliseconds) of the animation.
  • timeline: Gets or sets the current timeline of the animation. Defaults to the document timeline (document.timeline). Support for this property is currently limited in all browsers.

Methods

  • cancel(): Cancels the animation.
  • finish(): Immediately completes an animation.
  • pause(): Pauses an animation.
  • play(): Plays an animation.
  • reverse(): Reverses the current direction of the animation and plays it.

Event Handlers

  • oncancel: Triggered when the animation is canceled, such as by calling the cancel() method.
  • onfinish: Triggered when the animation is finished, such as by calling the finish() method.

Creating a simple scrubber using Web Animation API

By manipulating the currentTime property, the below adds a simple scrubber to the basic animation we've seen:

I create a HTML5 Range Slider to use as the scrubber. When the animation first runs (automatically), the animation's currentTime property value is continuously fed to the slider so the two are in sync. There is currently no "onprogress" event handler or anything similar (as far as I know) to only run code while the WAAPI animation is running, so I use requestAnimationFrame() instead to monitor the animation's progress. Once the animation has finished, I take advantage of the WAAPI event onfinish to call cancelAnimationFrame() and stop updating the slider needlessly.

Whenever the user interacts with the Ranger Slider, I update the WAAPI animation to sync with the slider:

scrubber.addEventListener('mousedown', ()=>{
	boxanimation.pause()
	updateScrubber()
})

scrubber.addEventListener('mouseup', ()=>{
	boxanimation.play()
})

scrubber.addEventListener('input', ()=>{
	boxanimation.currentTime = scrubber.value * animationlength
})

When the user mouses down on the slider, I pause the animation and update the slider's value to sync with the animation's currentTime property. While the user is dragging the slider, the reverse happens- I sync the currentTime property to reflect the slider's value so the former is dependant on the later. And finally, when the user mouses up on the slider, I resume automatic playback of the animation.

Animating Multiple Elements at Once

In the next example, I'll demonstrate animating multiple elements at once using WAAPI, and performing an action after they have all ended.

Note: Native support for WAAPI promises is spotty at the time of writing, even in Chrome and FF. I had to use the Web Animation Next Polyfill to get the feature to work across browsers.

There's nothing too complicated going on here. Basically I loop through and call animate() on each letter within a headline, and store each Animation object instance inside an array. With this array, I can cycle and play the series of animations on demand. Each animation's finished property returns a Promise that's resolved when that animation finishes playing, which I take advantage with Promise.all() to reset the entire animation when all of them have completed playing.

Creating an Animation using the Animation() constructor function

So far I've only created WAAPI animations using the animate() object directly on an element, which returns an Animation object instance. I'd be remiss however not to mention that you can also use the Animation() constructor function to accomplish the same thing.

Note: Native support for Animation() is spotty at the time of writing, even in Chrome and FF. I had to use the Web Animation Next Polyfill to get the feature to work across browsers.

With the caveat out of the way, here is how to call the Animation() constructor function:

var myanimation = new Animation([effect][, timeline]);

The function accepts two arguments:

  • effect: The animation effect. At the time of writing, only the keyframeEffect object is supported.
  • timeline: The animation timeline. At the time of writing, only document.timeline is supported.

Lets see how this works with a simple example:

Here's the JavaScript code:

var boxref = document.getElementById("box")

var boxkeyFrames = new KeyframeEffect( 
	boxref, // element to animate
	[
		{ transform: 'translateX(0) rotate(0deg)', background:'red' }, // keyframe 
		{ transform: 'translateX(90vw) rotate(180deg)', background:'blue' }
	], 
		{ duration: 2000, fill: 'forwards', iterations: 5 } // keyframe options
);

var boxanimation = new Animation(boxkeyFrames, document.timeline)

boxanimation.play()

The new KeyframeEffect() object is an all-in-one object that contains all the settings of an animation in one place, from the target element, the keyframes to use, to the keyframe options.

Are You Ready for WAAPI?

WAAPI when fully mature brings the versatility and ease of animating elements with CSS keyframes into the JavaScript environment. While some of the more advanced features are not yet implemented in modern browsers, with the right polyfill, you can start taking advantage of WAAPI today. I'm really excited to see the day we no longer have to turn to external libraries like jQuery or Anime to create complex animations.

Recommended Reading: