Programmatic animations beyond the limits of CSS transitions
When animations depend on runtime values, need to be interruptible, or must synchronize with other animations, pure CSS hits its limits. The Web Animations API solves exactly that, with the same performance as native CSS animations.
Table of Contents
- 1. Where CSS Animations Hit Their Limits
- 2. element.animate(): Basics and Syntax
- 3. Controlling the Animation Object
- 4. Keyframes Computed at Runtime
- 5. Timelines and Synchronizing Multiple Animations
- 6. The finished Promise and Chaining Animations
- 7. Performance: Compositor Thread Instead of Main Thread
- 8. When CSS, When WAAPI?
- 9. Best Practices and Summary
- 10. Summary
- 11. FAQ
1. Where CSS Animations Hit Their Limits
CSS transitions and keyframe animations cover most UI animations well: hover effects, simple fade in/out, fixed loading animations. But as soon as values need to be computed at runtime, for example the target position of a drag-and-drop element, the remaining distance of a scroll progress, or a physics-based bounce effect, pure CSS becomes unwieldy: keyframes would have to be dynamically generated as style strings.
The Web Animations API (WAAPI) solves this problem by modeling animations as JavaScript objects instead of static CSS. element.animate(keyframes, options) creates an animation that can be paused, reversed, sped up or slowed down, or synchronized with other animations programmatically, all while using the same compositor engine under the hood as CSS animations.
2. element.animate(): Basics and Syntax
The method element.animate(keyframes, options) takes as its first argument an array of keyframe objects, or an object with property arrays, and as its second argument either a number (duration in milliseconds) or an options object with duration, easing, iterations, delay, and other fields. The return value is an Animation object through which the playback can be controlled.
Unlike CSS keyframes, WAAPI animations do not require named @keyframes blocks to exist in a stylesheet, the keyframes are defined directly in the JavaScript call. That is especially suited to animations whose intermediate values are only known at runtime, for example computed from a user interaction or a server response.
const box = document.querySelector('.box');
box.animate(
[
{ transform: 'translateX(0px)', opacity: 1 },
{ transform: 'translateX(200px)', opacity: 0.5 },
],
{ duration: 500, easing: 'ease-out', fill: 'forwards' }
);
3. Controlling the Animation Object
The return value of animate() is not a promise, it is an Animation object with methods like play(), pause(), reverse(), cancel(), and finish(), plus a writable playbackRate property. That lets you pause a running animation at any time, resume it in reverse, or end it immediately, something that is only possible with pure CSS via workarounds like class toggling.
A practical example is an animation that should reverse seamlessly instead of restarting when the user interacts again, for example a second click while the first animation is still running. With CSS you would have to read the currently computed style and construct a new transition from that point, with WAAPI a plain animation.reverse() suffices.
const animation = box.animate(keyframesArray, { duration: 400, fill: 'both' });
closeButton.addEventListener('click', () => {
if (animation.playState === 'running') {
animation.reverse();
} else {
animation.play();
}
});
4. Keyframes Computed at Runtime
The real advantage over CSS shows when keyframes depend on runtime data that is not known at the time styles are written. One example: an element should animate from its current on-screen position to the coordinates of another element determined only at runtime, for example an 'add to cart' fly-to-icon animation.
That involves first reading the start and target position via getBoundingClientRect(), computing the difference, and inserting it as a transform value into a dynamically generated keyframe array. CSS could only approximate this via generated inline custom properties, WAAPI accepts arbitrary JavaScript values directly as keyframe input, with no detour through a style string.
function flyToCart(icon, cartElement) {
const start = icon.getBoundingClientRect();
const end = cartElement.getBoundingClientRect();
const dx = end.left - start.left;
const dy = end.top - start.top;
icon.animate(
[
{ transform: 'translate(0, 0) scale(1)', opacity: 1 },
{ transform: `translate(${dx}px, ${dy}px) scale(0.3)`, opacity: 0 },
],
{ duration: 600, easing: 'cubic-bezier(0.4, 0, 1, 1)' }
);
}
5. Timelines and Synchronizing Multiple Animations
Every animation has a currentTime property that lets you directly read and set its progress, independent of the actual elapsed real time. That makes it possible to keep several animations exactly in sync, for example when a complex icon is made of several SVG paths that should all run through the same motion in lockstep.
For some years now, ScrollTimeline has additionally added a way to tie currentTime not to real time but to a container's scroll progress, letting scroll-driven animations run entirely without a scroll event listener and without any JavaScript computation load on the main thread, an area where CSS transitions fundamentally cannot keep up.
const animations = [iconPath1, iconPath2, iconPath3].map((el) =>
el.animate(sharedKeyframes, { duration: 800, fill: 'both' })
);
// Start all animations at exactly the same progress
animations.forEach((a) => (a.currentTime = 0));
6. The finished Promise and Chaining Animations
Every Animation object has a finished property, a promise that resolves once the animation ends normally, and rejects if it is cancelled. That lets you chain animations cleanly with async/await, without falling back on the animationend event with its more cumbersome listener syntax and the well-known problems with several animations running simultaneously on the same element.
For a sequence of several consecutive animation steps, for example fading in, a brief pause, fading out, that lets you write a linear, readable async function instead of nested setTimeout calls or a chain of .then() handlers that quickly becomes unwieldy whenever the order changes.
async function pulseAndFade(el) {
await el.animate(
[{ transform: 'scale(1)' }, { transform: 'scale(1.1)' }, { transform: 'scale(1)' }],
{ duration: 300 }
).finished;
await el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 400, fill: 'forwards' }).finished;
}
7. Performance: Compositor Thread Instead of Main Thread
A common misconception is that JavaScript-driven animations are inevitably slower than CSS animations because they 'come from JavaScript'. In reality, the Web Animations API uses the same rendering pipeline as native CSS animations for compositor-capable properties like transform and opacity: once the keyframes are handed to the engine, the actual interpolation runs on the compositor thread, independent of the main thread.
Concretely that means a WAAPI animation that only animates transform and opacity keeps running smoothly even while the main thread is blocked by heavy JavaScript computation, exactly like an equivalent CSS animation. If, on the other hand, layout-triggering properties like width, top, or margin are animated, both approaches incur the same performance cost from repeated layout and repaint, regardless of the chosen API.
8. When CSS, When WAAPI?
For simple, static state transitions like hover effects, focus rings, or fixed loading indicators, CSS remains the right choice: less code, better separation of presentation and logic, and the browser can already optimize the animation during style parsing. But as soon as animations depend on runtime values, need to be interruptible, or must be synchronized programmatically, WAAPI is the more precise choice.
A third option deserves mention: document.getAnimations() returns every currently running animation on the page, regardless of whether it was started via CSS or WAAPI. That enables hybrid approaches, for example CSS for the base animation with targeted JavaScript intervention only in exceptional cases, without having to reimplement the entire animation in JavaScript.
9. Best Practices and Summary
The Web Animations API is not a replacement for CSS animations, it is a complement for exactly the cases where CSS hits its limits: dynamically computed keyframes, programmatic control over running animations, and synchronizing several animations via a shared timeline. On the performance side, there is no downside compared to pure CSS for compositor-capable properties.
In practice a clear rule of thumb works well: keep static, predictable transitions in CSS, implement dynamic, interactive, or synchronized animations in WAAPI, and in both cases consistently favor transform and opacity over layout-triggering properties, to fully exploit compositor-thread performance either way.
| Criterion | CSS Animation | Web Animations API |
|---|---|---|
| Static keyframes | Ideal | Possible, but unnecessary |
| Runtime-dependent values | Only via workarounds (inline styles) | Natively supported |
| Programmatic control | Limited (class toggling) | Play/pause/reverse/rate directly |
| Compositor performance (transform/opacity) | Identical | Identical |
Mironsoft
Modern browser APIs, performance, and maintainable JavaScript
JavaScript that holds up in the real browser, not just in the tutorial?
We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.
Code Review
Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.
Performance Optimization
Improving bundle size, load time, and runtime performance with modern APIs.
Modernization
Deliberately introducing native browser APIs instead of heavy libraries.
10. Summary
Web Animations API: The Essentials at a Glance
Core method
element.animate(keyframes, options) creates an animation directly from JavaScript values, without @keyframes in the stylesheet.
Programmatic control
The returned Animation object allows play(), pause(), reverse(), and an adjustable playbackRate.
Performance
For transform and opacity, WAAPI runs on the compositor thread just like CSS animations, independent of the main thread.
Rule of thumb
Keep static transitions in CSS, implement dynamic, interactive, or synchronized animations via WAAPI.