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:
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:
|
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:
|
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 thecancel()
method.onfinish
: Triggered when the animation is finished, such as by calling thefinish()
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 thekeyframeEffect
object is supported.timeline
: The animation timeline. At the time of writing, onlydocument.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: