Controlling Playback
Web Animations API (WAAPI) moves beyond the binary on/off nature of CSS Transitions and Animations, offering granular control over playback. With CSS, you have a single point of actuation — once an animation starts, you’re largely locked in. WAAPI replaces that light switch with a dimmer slider, allowing you to pause, rewind, seek, and even redefine effects at runtime. This opens doors to dynamic effects that adapt to user interaction or context, and makes it feasible to build tools like an animation editor with a live preview.
The core interface for this control is the Animation object, which you get back from the Element.animate method. Think of this object as a playback device for a defined animation. Its methods and properties — such as play(), pause(), and currentTime — are the controls you use to manage the sequence. The definition of the animation itself (the keyframes and timing options) is packaged separately in a KeyframeEffect instance.
const animation = element.animate(keyframes, options);
The snippet above is a shortcut. Let’s break it down into its equivalent components to see what happens behind the scenes.
const animation = new Animation( // (2)
new KeyframeEffect(element, keyframes, options) // (1)
);
animation.play(); (3)
This separation of concerns — the effect definition from the playback controls — is critical for coordinating complex sequences. Instead of creating and launching animations on the fly, you can define all your KeyframeEffect objects ahead of time. This ensures they are ready to synchronize. Generating and playing animations simultaneously can lead to slight delays, which accumulate over a long sequence and ruin the experience.
Two Dimensions of Timing
Timing is the soul of animation, and WAAPI gives you control at two distinct levels: individual property timing and overall animation timing.
At the property level, the offset option in a keyframe grants precise control. This is akin to the percentage values in CSS @keyframes, but it is a fraction of the duration of a single iteration, with a value between 0 and 1. This relative positioning is powerful; it guarantees that keyframes maintain their proportional timing relative to each other, regardless of the animation’s duration or playback rate.
Calculating Total Time
It’s essential to understand that the duration option is not necessarily the animation’s total length. Duration is the time for one iteration to complete. You must account for delays and repetition to find the true overall time, represented by the following equation.
delay + (iterations × duration) + end delay
You can see this in practice with coordinated effects:
See the Pen [What is the actual duration of an animation?](https://codepen.io/smashingmag/pen/VwWWrzz) by Kirill Myshkin.
This formula allows you to align multiple animations within a fixed context, much like a piece of media of a known length. By using delay at the start and padding at the end, you can embed an animation of a specific duration into a longer overall timeline. This functions as a sort of macro-level offset for the entire animation.
Another valuable timing option is iterationStart. This allows you to define where in the animation’s sequence it begins, effectively shifting it along its own timeline. For instance, you could set a ball to begin its bounce cycle from the middle of the screen rather than the beginning, as demonstrated in the following example.
See the Pen [Tweak interationStart](https://codepen.io/smashingmag/pen/qBjjVPR) by Kirill Myshkin.
Grouping Animations Into a Single Control Surface
Building an animation editor for a presentation app surfaced an early design problem: arranging multiple animations for one element on a timeline. The first instinct was to use offset to place an animation at the correct starting point. That approach backfired. Moving an animation on the timeline meant shifting its start position without changing its duration, which required adjusting both the offset and the closing property's offset. The bookkeeping quickly became unmanageable.
The transform property presented a second obstacle. Because it chains several functions in sequence, the order matters. For example, scale followed by translate behaves differently than the reverse, especially when translate is expressed in percentages relative to element size. A ball meant to jump exactly three times its own height will land differently depending on whether the scale function precedes or follows the translation.
That ordering trait is valuable for complex single transformations, but it becomes a constraint when you need independent, parallel effects. Keyframes arriving from different sources would require intricate merging of a transformed string, and relying on an automatic merger is impractical because the logic is not straightforward. Visualizing the intended result also gets harder.
A cleaner approach is to separate effects into distinct channels by wrapping elements in dedicated divs — one for positioning, another for scaling, a third for rotation. This not only simplifies each animation definition but also permits different transform origins where needed. At first glance, adding wrapper elements seems like it multiplies complexity. That was the initial reaction here too; writing a compiler that assembled a single transform string in the right order seemed more elegant. One additional transform function made that compiler logic too convoluted, and some effects proved impossible to achieve that way, so the wrapper strategy won.
Building a Player for Many Animations
Controlling several animations together turns out to be straightforward. The only difference between calling a method on one animation and on an array of animations is iterating over the list.
// To play just call play on all of them
animations.forEach((animation) => animation.play());
That pattern extends to any method of Animation instances. A createPlayer function can accept an array of animations to be played in sync and return an object with methods for control.
function createPlayer(animations) {
return Object.freeze({
play: function () {
animations.forEach((animation) => animation.play());
}
});
}
Adding pause and current-time adjustment widens the control surface.
function createPlayer(animations) {
return Object.freeze({
play: function () {
animations.forEach((animation) => animation.play());
},
pause: function () {
animations.forEach((animation) => animation.pause());
},
currentTime: function (time = 0) {
animations.forEach((animation) => animation.currentTime = time);
}
});
}
Because the Animation interface resembles media-player interfaces, the player can also accept other player objects. Adjusting the currentTime method to work with both animations and nested player objects makes this composition possible.
function currentTime(time = 0) {
animations.forEach(function (animation) {
if (typeof animation.currentTime === "function") {
animation.currentTime(time);
} else {
animation.currentTime = time;
}
});
}
This player abstraction hides the complexity of managing several divs as animation channels for a single element. Those elements can then be grouped into scenes, and scenes can be composed into larger structures.
In a timing demonstration, all animations were divided into three players. The first controls playback of a preview panel. The second combines the jumping animations for all ball outlines — those in the left container and the one in the preview. The third groups the position animations of the balls in the left container, enabling a continuous spread demonstration rendered in roughly 60-frames-per-second slices.
Managing Complexity, Not Hiding It
Web Animations API exposes capabilities browsers already perform internally, such as offloading work to the GPU. The API grants explicit control over timing and playback, and while that control can initially feel foreign, it does not have to make the code confusing. Understanding timing and playback mechanics gives you the tools to shape the API around your needs, defining exactly how much complexity is necessary.
Further Reading
- “Practical Techniques On Designing Animation,” Sarah Drasner
- “Designing With Reduced Motion For Motion Sensitivities,” Val Head
- “An Alternative Voice UI To Voice Assistants,” Ottomatias Peura
- “Designing Better Tooltips For Mobile User Interfaces,” Eric Olive




