Orchestrating animations with promises, performance improvements with replaceable animations, smoother animations with composite modes, and more.
Published: May 27, 2020
When used correctly, animations improve user perception and memory of your brand, guide user actions, and help users navigate your application—providing context in a digital space.
The Web Animations API is a tool that enables developers to write imperative animations with JavaScript. It was written to underpin both CSS animation and transition implementations and enable future effects to be developed, as well as existing effects to be composed and timed.
While Firefox and Safari have already implemented the full set of spec features, Chromium 84 brings a slew of previously unsupported features to Chrome and Edge enabling cross-browser interoperability.
Getting started
Creating an animation using the Web Animations API should feel very familiar if you've used @keyframe
rules. First you'll need to create a Keyframe Object. What might look like this in CSS:
@keyframes openAnimation {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
would look like this in JavaScript:
const openAnimation = [
{ transform: 'scale(0)' },
{ transform: 'scale(1)' },
];
Where you set parameters for animation in CSS:
.modal {
animation: openAnimation 1s 1 ease-in;
}
you would set in JS:
document.querySelector('.modal').animate(
openAnimation, {
duration: 1000, // 1s
iterations: 1, // single iteration
easing: 'ease-in' // easing function
}
);
The amount of code is about the same, but with JavaScript, you get a couple of superpowers that you don't have with CSS alone. This includes the ability to sequence effects, and an increased control of their play states.
Beyond element.animate()
However, with the update, the Web Animations API is no longer restricted to animations created using element.animate()
. We can manipulate CSS animations and transitions as well.
getAnimations()
is a method that returns all animations on an element regardless of whether it was created using element.animate()
or using CSS rules (CSS animation or transition). Here is an example of what this looks like:
You first "get"
the keyframes for the transition to determine where we are transitioning from. Then, you create two new opacity animations, enabling the cross fade effect. Once the cross-fade completes, you delete the copy.
How to orchestrate animations with promises
In Chromium 84, you now have two methods that can be used with promises: animation.ready
and animation.finished
.
animation.ready
let you wait for pending changes to take effect (that is, switching between playback control methods such as play and pause).animation.finished
provides a means of executing custom JavaScript code when an animation is complete.
Continuing on with our example, and create an orchestrated animation chain with animation.finished
. Here, you have a vertical transformation (scaleY
), followed by a horizontal transformation (scaleX
), followed by an opacity change on a child element:
const transformAnimation = modal.animate(openModal, openModalSettings);
transformAnimation.finished.then(() => { text.animate(fadeIn, fadeInSettings)});
We've chained these animations using animation.finished.then()
prior to executing the next animation set in the chain. This way, the animations appear in order, and you are even applying effects to different target elements with different options set (such as speed and ease).
Within CSS, this would be cumbersome to recreate, especially when applying unique, yet sequenced animations to multiple elements. You'd have to use a @keyframe
, sort out the correct timing percentages to place the animations, and use animation-delay
prior to triggering the animations in the sequence.
Example: Play, pause, and reverse
What can open, should close! Luckily, since Chromium 39, the Web Animations API has provided us the ability to play, pause, and reverse our animations.
You can take the animation previously shown, and give it a smooth, reversed animation when clicking the button again using .reverse()
. This way, you can create a smoother and more contextual interaction for our modal.
What you can do is create two play-pending animations (openModal
, and an inline opacity transformation), and then pause one of the animations, delaying it until the other is finished. You can then use promises to wait for each to be finished before playing. Finally, you can check to see if a flag is set, and then reverse each animation.
Example: Dynamic interactions with partial keyframes
selector.animate([{transform: `translate(${x}px, ${y}px)`}],
{duration: 1000, fill: 'forwards'});
In this example, there is only one keyframe, and no specified start position. This is an example of using partial keyframes. The mouse handler does a few things here: it sets a new end location and triggers a new animation. The new start position is inferred from the current underlying position.
New transitions can be triggered while existing ones are still running. This means that the current transition is interrupted, and a new one is created.
Performance improvements with replaceable animations
When creating animations based on events, such as on 'mousemove'
, a new animation is created each time, which can quickly consume memory and degrade performance. To address this problem, replaceable animations were introduced in Chromium 83, enabling automated cleanup, where finished animations are flagged as replaceable and automatically removed if replaced by another finished animation. Consider the following example:
elem.addEventListener('mousemove', evt => {
rectangle.animate(
{ transform: translate(${evt.clientX}px, ${evt.clientY}px) },
{ duration: 500, fill: 'forwards' }
);
});
Each time the mouse moves, the browser re-calculates the position for each ball in the comet trail and creates an animation to this new point. The browser now knows to remove old animations (enabling replacement) when:
- The animation is finished.
- There is one or more animations higher in composite ordering that are also finished.
- The new animations are animating the same properties.
You can see exactly how many animations are being replaced by tallying up a counter with each removed animation, using anim.onremove
to trigger the counter.
There are a few additional methods to take your animation control even further:
animation.replaceState()
provides a means of tracking whether an animation is active, persisted, or removed.animation.commitStyles()
updates the style of an element based on the underlying style along with all animations on the element in the composite order.animation.persist()
marks an animation as non-replaceable.
Smoother animations with composite modes
With the Web Animations API, you can now set the composite mode of your animations, meaning they can be additive or accumulative, in addition to the default mode of "replace". Composite modes allow developers to write distinct animations and have control over how effects are combined. Three composite modes are now supported: 'replace'
(the default mode), 'add'
, and 'accumulate'
.
When you composite animations, a developer can write short, distinct effects and see them combined together. In the following example, we are applying a rotation and scale keyframe to each box, with the only adjustment being the composite mode, added as an option:
In the default 'replace'
composite mode, the final animation replaces the transform property and ends up at rotate(360deg) scale(1.4)
. For 'add'
, composite adds the rotation and multiplies the scale, resulting in a final state of rotate(720deg) scale(1.96)
. 'accumulate'
combines the transformations, resulting in rotate(720deg) scale(1.8)
. For more on the intricacies of these composite modes, check out The CompositeOperation and CompositeOperationOrAuto enumerations from the Web Animations spec.
Take a look at the following UI element example:
Here, two top
animations are composited. The first is a macro-animation, which moves the drop-down by the full height of the menu itself as a slide-in effect from the top of the page, and the second, a micro-animation, applies a little bounce as it hits the bottom. Using the 'add'
composite mode enables a smoother transition.
const dropDown = menu.animate(
[
{ top: `${-menuHeight}px`, easing: 'ease-in' },
{ top: 0 }
], { duration: 300, fill: 'forwards' });
dropDown.finished.then(() => {
const bounce = menu.animate(
[
{ top: '0px', easing: 'ease-in' },
{ top: '10px', easing: 'ease-out' },
{ ... }
], { duration: 300, composite: 'add' });
});
What's next for the Web Animations API
These are all exciting additions to animations capabilities in today's browsers, and even more additions are coming down the pipeline. Check out these future specifications for some further reading on what's coming next: