Web Animations in Safari 13.1

With the release of iOS 13.4, iPadOS 13.4, and Safari 13.1 in macOS Catalina 10.15.4, web developers have a new API at their disposal: Web Animations. We have been working on this feature for well over 2 years and it’s now available to all Safari users, providing a great programmatic API to create and control animations using JavaScript.

In this article we will discuss the benefits of this new API, how best to detect its availability, how it integrates with existing features such as CSS Animations and CSS Transitions, and the road ahead for animation technology in WebKit.

A Little History

The WebKit team came up with original proposals for CSS Animations and CSS Transitions back in 2007 and announced them on this blog. Over the years these specifications have matured and become W3C standards and an integral part of the web platform.

With these technologies, integrating animations in web content became simple, removing the requirement for developers to write JavaScript while providing better performance and power consumption by allowing browsers to use hardware acceleration when available and integrate animations in the layout and rendering pipeline.

As a web developer, I’ve enjoyed the simplicity and great performance of CSS Animations and CSS Transitions. I believe it is those virtues that have allowed animations to become a powerful tool for web developers. However, in my day-to-day work, I also found a few areas where these technologies were frustrating: dynamic creation, playback control, and monitoring an animation’s lifecycle.

The great news is that these issues are all taken care of by the new Web Animations API. Let’s see how to leverage this new API to improve everyday code in these areas.

Part I – Animation Creation

While CSS allows you to very easily animate a state change (the appearance of a button, for instance) it will be a lot trickier if the start and end values of a given animation are not known ahead of time. Typically, web developers would deal with those cases with CSS Transitions:

// Set the transition properties and start value.
element.style.transitionProperty = "transform";
element.style.transitionDuration = "1s";
element.style.transform = "translateX(0)";

// Force a style invalidation such that the start value is recorded.
window.getComputedStyle(element);

// Now, set the end value.
element.style.transform = "translateX(100px)";

While this may look like a reasonable amount of code, there are additional factors to consider. Forcing a style invalidation will not let the browser perform that task at the time it will judge most appropriate. And this is just one single animation; what if another part of the page, possibly even a completely different JavaScript library, also needed to create an animation? This would multiply forced style invalidations and degrade performance.

And if you consider using CSS Animations instead, you would have to first generate a dedicated @keyframes rule and insert it inside a <style> element, failing to encapsulate what is really a targeted style change for a single element and causing a costly style invalidation.

The value of the Web Animations API lies in having a JavaScript API that preserves the ability to let the browser engine do the heavy lifting of running animations efficiently while enabling more advanced control of your animations. Using the Web Animations API, we can rewrite the code above with a single method call using Element.animate():

element.animate({ transform: ["translateX(0)", "translateX(100px)"] }, 1000);

While this example is very simple, the single Element.animate() method is a veritable Swiss Army knife and can express much more advanced features. The first argument specifies CSS values while the second argument specifies the animation’s timing. We won’t go into all possible timing properties, but all of the features of CSS Animations can be expressed using the Web Animations API. For instance:

element.animate({
    transform: ["translateX(500px)"], // move by 500px 
    color: ["red", "blue", "green"]   // go through three colors
}, {
    delay: 500,            // start with a 500ms delay
    easing: "ease-in-out", // use a fancy timing function 
    duration: 1000,        // run for 1000ms
    iterationCount: 2,     // repeat once
    direction: "alternate" // run the animation forwards and then backwards
});

Now we know how to create an animation using the Web Animations API, but how is it better than the code snippet using CSS Transitions? Well, that code tells the browser what to animate and how to animate it, but does not specify when. Now the browser will be able to process all new animations at the next most opportune moment with no need to force a style invalidation. This means that animations you author yourself as well as animations that may originate from a third-party JavaScript library – or even in a different document (for instance, via an <iframe>) – will all be started and progress in sync.

Part II – Playback Control

Another shortcoming with existing technologies was the lack of playback control: the ability to pause, resume, and seek animations and control their speed. While the animation-play-state property allows control of whether a CSS Animation is paused or playing, there is no equivalent for CSS Transitions and it only controls one aspect of playback control. If you want to set the current time of an animation, you can only resort to roundabout techniques such as clever manipulations of negative animation-delay values, and if you want to change the speed at which an animation plays, the only option is to manipulate the timing values.

With the Web Animations API, all these concerns are handled by dedicated API. For instance, we can manipulate playback state using the play() and pause() methods, query and set time using the read-write currentTime property, and control speed using playbackRate without modifying duration:

// Create an animation and keep a reference to it.
const animation = element.animate(…);

// Pause the animation.
animation.pause();

// Change its current time to move forward by 500ms.
animation.currentTime += 500;

// Slow the animation down to play at half-speed.
animation.playbackRate = 0.5;

This gives developers control over the behavior of animations after they have been created. It is now trivial to perform tasks which would have been previously daunting. To toggle the playback state of an animation at the press of a button:

button.addEventListener("click", event => {
    if (animation.playState === "paused")
        animation.play();
    else
        animation.pause(); 
});

To connect the progress of an animation to an <input type="range"> element:

input.addEventListener("input", event => {
    animation.currentTime = event.target.value * animation.effect.getTiming().duration;
});

Thanks to the Web Animations API making playback control a core concept for animations, these simple tasks are trivial and more complex control over an animation’s state can be achieved.

Part III – Animation Lifecycle

While the transition* and animation* family of DOM events provide information about when CSS-originated animations start and end, it is difficult to use them correctly. Consider fading out an element prior to removing it from the DOM. Typically, this would be written this way using CSS Animations:

@keyframes fade-out {
    to { opacity: 0 }
}
element.style.animationName = "fade-out";
element.addEventListener("animationend", event => {
    element.remove();
});

Seems correct, but on further inspection there are problems. This code will remove the element as soon as an animationend event is dispatch on the element, but since animation events bubble, the event could come from an animation completing in a child element in the DOM hierarchy, and the animations could even be named the same way. There are measures you can take to make this kind of code safer, but using the Web Animations API, writing this kind of code is not just easier but safer because you have a direct reference to an Animation object rather than working through animation events scoped to an element’s hierarchy. And on top of this, the Web Animations API uses promises to monitor the ready and finished state of animations:

let animation = element.animate({ opacity: 0 }, 1000);
animation.finished.then(() => {
    element.remove();
});

Consider how complex the same task would have been if you wanted to monitor the completion of a number of CSS Animations targeting several elements prior to removing a shared container. With the Web Animations API and its support for promises this is now expressed concisely:

// Wait until all animations have finished before removing the container.
let animations = container.getAnimations();
Promise.all(animations.map(animation => animation.finished).then(() => {
    container.remove();
});

Integration with CSS

Web Animations are not designed to replace existing technologies but rather to tightly integrate with them. You are free to use whichever technology you feel fits your use case and preferences best.

The Web Animations specification does not just define an API but also aims to provide a shared model for animations on the web; other specifications dealing with animations are defined with the same model and terminology. As such, it’s best to understand Web Animations as the foundation for animations on the web, and think of its API as well as CSS Transitions and CSS Animations as layers above that shared foundation.

What does this mean in practice?

To make a great implementation of the Web Animations API, we had to start off fresh with a brand new and shared animation engine for CSS Animations, CSS Transitions, and the new Web Animations API. Even if you don’t use the Web Animations API, the CSS Animations and CSS Transitions you’ve authored are now running in the new Web Animations engine. No matter which technology you choose, the animations will all run and update in sync, events dispatched by CSS-originated animation and the Web Animations API will be delivered together, etc.

But what may matter even more to authors is that the entire Web Animations API is available to query and control CSS-originated animations! You can specify animations in pure CSS but also control them with the Web Animations APIs using Document.getAnimations() and Element.getAnimations(). You can pause all animations running for a given document this way:

document.getAnimations().forEach(animation => animation.pause());

What about SVG? At this stage, SVG Animations remain distinct from the Web Animations model, and there is no integration between the Web Animations API and SVG. This remains an area of improvement for the Web platform.

Feature Detection

But before you start adopting this new technology in your projects, there are some further practical considerations that you need to be aware of.

Since this is new technology, it is important to use feature detection as users gradually update their browsers to newer versions with support for Web Animations. Detecting the availability of the various Web Animations API is simple. Here is one correct way to detect the availability of Element.animate():

if (element.animate) 
    element.animate(…); // Use the Web Animations API.
else// Fall back to other technologies.

While Safari is shipping the entire Web Animations API as a whole, other browsers, such as Firefox and Chrome, have been shipping Element.animate() for a long time already, so it’s critical to test individual features separately. So, if you want to use Document.getAnimations() to query all running animations for a given document, make sure to feature detect that feature’s availability. As such the snippet further above would be better written this way:

if (document.getAnimations)
   document.getAnimations().forEach(animation => animation.pause());
else// Fall back to another approach.

There are parts of the API that aren’t yet implemented in Safari. Notably, effect composition is not supported yet. Before trying to set the composite property when defining the timing of your animation, you can check whether it is supported this way:

const isEffectCompositionSupported = !!(new KeyframeEffect(null, {})).composite;

Animations in Web Inspector

Also new in the latest Safari release: CSS Animations and CSS Transitions can be seen in Web Inspector in the new Media & Animations timeline in the Timelines Tab, organized by animation-name or transition-property properties for each element target. When used alongside other timelines, it can be helpful to correlate how that particular CSS Animation or CSS Transition was created, such as by looking at script entries in the JavaScript & Events timeline.

Web Inspector Media and Animations Timeline

Starting in Safari Technology Preview 100, Web Inspector shows all animations, whether they are created by CSS or using the JavaScript API, in the Graphics Tab. It visualizes each individual animation object with lines for delays and curves for keyframes, and provides an in-depth view of exactly what the animation will do, as well as information about how it was created and some useful actions, such as logging the animation object in the Console. These are the first examples of what Web Animations allow to improve Web Inspector for working with animations, and we’re looking forward to improving our tools further.

Web Inspector Graphics Tab in Light Mode

The Road Ahead

Shipping support for Web Animations is an important milestone for animations in WebKit. Our new animation engine provides a more spec-compliant and forward-looking codebase to improve on. This is where developers like you come in: it’s important we hear about any compatibility woes you may run into with existing content using CSS Animations and CSS Transitions, but also when adopting the new Web Animations API in your content.

The transition to the new Web Animations engine allowed us to address known regressions and led to numerous progressions with Web Platform Tests, improving cross-browser compatibility. If you find your animations running differently in Safari, please file a bug report on bugs.webkit.org so that we can diagnose the issue and establish if it is an intentional change in behavior or a regression that we should address.

We’re already working on improving on this initial release and you can keep an eye out for future improvements by monitoring this blog and the release notes for each new Safari Technology Preview release.

You can also send a tweet to @webkit or @jonathandavis to share your thoughts on our new support for Web Animations.