Skip to main content

Web Animations API Essentials

The Web Animations API (WAAPI) is a JavaScript API that bridges CSS animation and JavaScript animation, giving you full control without requiring a third-party library. Instead of manually implementing animation loops with requestAnimationFrame, you define keyframes and timing, then let the browser orchestrate the animation. WAAPI animations run on the compositor thread when possible, ensuring 60 fps performance. This article teaches you how to use WAAPI in React for practical animations with play, pause, and synchronization control.

Core Concept: element.animate()

The simplest WAAPI method is element.animate(), which takes two arguments: keyframes and options.

const animation = element.animate(
[
{ transform: 'translateX(0px)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0.5 },
],
{
duration: 1000, // milliseconds
easing: 'ease-out-cubic',
fill: 'forwards', // keep the last keyframe applied
}
);

The keyframes array is an array of objects, where each object represents a point in the animation. By default, WAAPI distributes keyframes evenly across the duration. Here is a more detailed example in React:

import { useEffect, useRef } from 'react';

export default function SimpleWAAPIAnimation() {
const boxRef = useRef(null);
const animationRef = useRef(null);

useEffect(() => {
if (!boxRef.current) return;

animationRef.current = boxRef.current.animate(
[
{ transform: 'scale(1)', backgroundColor: '#4ecdc4' },
{ transform: 'scale(1.5)', backgroundColor: '#ff6b6b' },
{ transform: 'scale(1)', backgroundColor: '#4ecdc4' },
],
{
duration: 800,
easing: 'ease-in-out',
iterations: 3, // repeat 3 times
}
);

return () => {
animationRef.current?.cancel();
};
}, []);

return (
<div
ref={boxRef}
style={{
width: '100px',
height: '100px',
backgroundColor: '#4ecdc4',
}}
/>
);
}

This component animates a box that scales and changes color 3 times.

Keyframe Formats

WAAPI supports two keyframe formats:

Format 1: Array of Objects (Most Common)

[
{ transform: 'translateX(0px)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0.5 },
]

Format 2: Object with Property Arrays

{
transform: ['translateX(0px)', 'translateX(100px)'],
opacity: [1, 0.5],
}

Both are equivalent. Use whichever reads more clearly in your code.

Animation Options

Here is a complete reference of animation options:

const options = {
duration: 1000, // Total animation time in milliseconds
delay: 0, // Delay before animation starts
easing: 'ease-in-out', // Timing function (CSS easing or custom cubic-bezier)
fill: 'forwards', // 'none' | 'forwards' | 'backwards' | 'both'
direction: 'normal', // 'normal' | 'reverse' | 'alternate' | 'alternate-reverse'
iterations: 1, // Number of times to repeat (can be Infinity)
iterationStart: 0, // Offset to start within an iteration (0–1)
composite: 'replace', // 'replace' | 'add' | 'accumulate'
iterationComposite: 'replace', // How iterations combine
};

Playback Control

WAAPI gives you full control over animation playback:

import { useEffect, useRef, useState } from 'react';

export default function PlaybackControlAnimation() {
const boxRef = useRef(null);
const animationRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(true);

useEffect(() => {
if (!boxRef.current) return;

animationRef.current = boxRef.current.animate(
[
{ transform: 'translateX(0px)' },
{ transform: 'translateX(300px)' },
],
{
duration: 2000,
easing: 'ease-in-out',
iterations: Infinity,
}
);
}, []);

const handlePlay = () => {
if (animationRef.current) {
animationRef.current.play();
setIsPlaying(true);
}
};

const handlePause = () => {
if (animationRef.current) {
animationRef.current.pause();
setIsPlaying(false);
}
};

const handleReverse = () => {
if (animationRef.current) {
animationRef.current.playbackRate *= -1;
}
};

const handleSeek = (time) => {
if (animationRef.current) {
animationRef.current.currentTime = time;
}
};

return (
<div>
<div
ref={boxRef}
style={{
width: '50px',
height: '50px',
backgroundColor: '#4ecdc4',
}}
/>
<div style={{ marginTop: '20px' }}>
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
<button onClick={handleReverse}>Reverse</button>
<button onClick={() => handleSeek(0)}>Reset</button>
<button onClick={() => handleSeek(1000)}>Seek to 1s</button>
</div>
</div>
);
}

Waiting for Animation Completion

WAAPI animations return a Promise that resolves when the animation finishes:

import { useEffect, useRef } from 'react';

export default function SequentialAnimations() {
const boxRef = useRef(null);

useEffect(() => {
const animate = async () => {
// Animation 1: scale up
const anim1 = boxRef.current.animate(
[{ transform: 'scale(1)' }, { transform: 'scale(2)' }],
{ duration: 500 }
);
await anim1.finished;

// Animation 2: rotate
const anim2 = boxRef.current.animate(
[{ transform: 'scale(2) rotate(0deg)' }, { transform: 'scale(2) rotate(360deg)' }],
{ duration: 800 }
);
await anim2.finished;

// Animation 3: scale down
const anim3 = boxRef.current.animate(
[{ transform: 'scale(2)' }, { transform: 'scale(1)' }],
{ duration: 500 }
);
await anim3.finished;

console.log('All animations complete!');
};

animate();
}, []);

return (
<div
ref={boxRef}
style={{
width: '100px',
height: '100px',
backgroundColor: '#4ecdc4',
}}
/>
);
}

Grouping Animations

Animate multiple elements simultaneously using Promise.all():

import { useEffect, useRef } from 'react';

export default function GroupedAnimations() {
const box1Ref = useRef(null);
const box2Ref = useRef(null);
const box3Ref = useRef(null);

useEffect(() => {
const animateAll = async () => {
const promises = [
box1Ref.current.animate(
[{ transform: 'translateX(0px)' }, { transform: 'translateX(100px)' }],
{ duration: 1000 }
).finished,
box2Ref.current.animate(
[{ opacity: 1 }, { opacity: 0 }],
{ duration: 1000 }
).finished,
box3Ref.current.animate(
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
{ duration: 1000 }
).finished,
];

await Promise.all(promises);
console.log('All animations complete');
};

animateAll();
}, []);

return (
<div style={{ display: 'flex', gap: '20px' }}>
<div
ref={box1Ref}
style={{ width: '50px', height: '50px', backgroundColor: '#4ecdc4' }}
/>
<div
ref={box2Ref}
style={{ width: '50px', height: '50px', backgroundColor: '#ff6b6b' }}
/>
<div
ref={box3Ref}
style={{ width: '50px', height: '50px', backgroundColor: '#ffe66d' }}
/>
</div>
);
}

Performance Comparison: WAAPI vs requestAnimationFrame

AspectWAAPIrequestAnimationFrame
Compositor optimizationAutomaticManual (if animating compositor properties)
Code verbosityLowHigher
Playback controlBuilt-in (play, pause, seek)Manual
SynchronizationEasyModerate
Browser supportAll modern browsersAll browsers
Learning curveModerateModerate

WAAPI is generally preferred for simple animations because it requires less boilerplate and offers built-in controls. Use requestAnimationFrame when you need frame-by-frame calculations or complex interactions.

Key Takeaways

  • The Web Animations API (element.animate()) provides a high-level interface to orchestrate animations without manual rAF loops.
  • WAAPI animations are optimized by the browser and run on the compositor when possible, ensuring 60 fps performance.
  • Use keyframes to define animation steps and options to control duration, easing, iteration, and playback direction.
  • WAAPI offers built-in playback control: play(), pause(), reverse(), and currentTime seeking.
  • Await the .finished promise to sequence animations or react when an animation completes.
  • Group multiple animations with Promise.all() for synchronized motion across multiple elements.

Frequently Asked Questions

What is the difference between WAAPI and CSS animations?

CSS animations are static and cannot be paused or reversed from JavaScript (without hacks). WAAPI animations can be paused, reversed, and seeked dynamically. Additionally, WAAPI offers more timing options and easier synchronization with other animations. Use CSS animations for simple, fire-and-forget motion; use WAAPI when you need control.

Can I animate custom CSS properties with WAAPI?

Yes. WAAPI supports CSS custom properties (CSS variables):

element.animate(
[
{ '--my-color': 'red' },
{ '--my-color': 'blue' },
],
{ duration: 1000 }
);

However, the browser must explicitly register the custom property with CSS.registerProperty() for smooth animation (otherwise it snaps between values). This is an advanced feature rarely needed.

Does WAAPI work with React state?

WAAPI animations bypass React's render cycle. If you need to sync animation with React state, capture the animation's completion with .finished and update state in the .then() callback. This can cause flickering; for smooth results, avoid mixing WAAPI with React state updates on the same properties.

How do I use WAAPI with Framer Motion or other animation libraries?

Most animation libraries (Framer Motion, React Spring) use WAAPI or rAF under the hood. There is no benefit to mixing WAAPI directly with these libraries; choose one and use it consistently. WAAPI is best for custom animations where you do not want external dependencies.

Can I animate SVG elements with WAAPI?

Yes, WAAPI works on SVG elements. Animate SVG attributes using camelCase (e.g., cx becomes cx, not cx attribute). CSS properties (like transform) also work on SVG:

svgElement.animate(
[{ transform: 'scale(1)' }, { transform: 'scale(2)' }],
{ duration: 1000 }
);

Further Reading