The Web Animations API

· 5 min read · Updated April 6, 2026 · intermediate
web-animations dom css animation browser

The Web Animations API (WAAPI) gives you a JavaScript interface to the browser’s animation engine. Instead of relying on CSS keyframes alone or a third-party library like GSAP, you can create, control, and reverse animations directly from JS. The browser handles the heavy lifting — WAAPI animations run on the compositor thread where possible, which keeps them smooth even when the main thread is busy.

The animate() Shortcut

The quickest way to start an animation is calling animate() on any DOM element:

element.animate(keyframes, options);

This returns an Animation object immediately, so you have full control from the first frame.

const box = document.querySelector('.box');

const animation = box.animate([
  { transform: 'translateX(0)', opacity: 1 },
  { transform: 'translateX(200px)', opacity: 0.5 },
  { transform: 'translateX(400px)', opacity: 1 }
], {
  duration: 800,
  easing: 'ease-in-out',
  fill: 'forwards'
});

After running this, box slides right 400px and stays there because of fill: 'forwards'.

Keyframe Formats

WAAPI accepts three keyframe formats:

Array of objects — each object is a keyframe at sequential offsets:

el.animate([
  { opacity: 0 },        // offset 0 (start)
  { opacity: 1 },        // offset 0.5 (middle)
  { opacity: 0 }         // offset 1 (end)
], { duration: 500 });

Object notation — properties map to arrays of values, offsets are calculated automatically:

el.animate({
  opacity: [0, 1, 0],
  transform: ['scale(1)', 'scale(1.2)', 'scale(1)']
}, { duration: 600, iterations: 3 });

Explicit offsets — mix property values with offset and easing:

el.animate([
  { opacity: 0, offset: 0 },
  { opacity: 1, offset: 0.8, easing: 'ease-out' },
  { opacity: 0, offset: 1 }
], { duration: 1000 });

Offset values range from 0 to 1. Any keyframe without an explicit offset gets one assigned evenly between its neighbors.

Animation Options

The second argument to animate() is an options object:

OptionDefaultDescription
duration0Length in milliseconds
delay0Start delay in milliseconds
fill'none''none', 'forwards', 'backwards', 'both'
iterations1Number of repetitions (use Infinity for looping)
direction'normal''normal', 'reverse', 'alternate', 'alternate-reverse'
easing'linear'CSS easing function name
iterationStart0Offset into the iteration timeline (e.g., 0.5 starts halfway)
composite'replace''add', 'replace', 'accumulate'
// Looping, alternating animation
el.animate([
  { transform: 'rotate(0deg)' },
  { transform: 'rotate(360deg)' }
], {
  duration: 2000,
  iterations: Infinity,
  direction: 'alternate',
  easing: 'ease-in-out'
});

Controlling Playback with the Animation Object

animate() returns an Animation object with methods to control playback:

const animation = el.animate(keyframes, { duration: 1000 });

// Pause and resume
animation.pause();
animation.play();

// Jump around
animation.currentTime = 500;    // seek to 500ms
animation.startTime = performance.now() - 300; // start 300ms ago

// Reverse
animation.reverse();

// Change speed
animation.updatePlaybackRate(2); // 2x speed

// Cancel completely
animation.cancel();

animation.playState tells you what’s happening: 'running', 'paused', 'finished', 'idle', or 'pending'.

You can listen for when animations finish:

animation.finished.then(() => {
  console.log('Animation done');
});

// Or via event listener
animation.addEventListener('finish', () => {
  console.log('Animation finished');
});

Filling: How the Animation Behaves Outside Its Active Time

The fill option controls what the element looks like before the animation starts and after it ends:

// Default — element reverts to pre-animation state
el.animate(keyframes, { duration: 500, fill: 'none' });

// Keeps final state after animation ends
el.animate(keyframes, { duration: 500, fill: 'forwards' });

// Shows first keyframe during the delay period
el.animate(keyframes, { duration: 500, delay: 200, fill: 'backwards' });

// Combines both — useful for responsive enter animations
el.animate(keyframes, { duration: 500, delay: 200, fill: 'both' });

Grouping and Scheduling with document.timeline

WAAPI provides a document.timeline that represents the default document time. This is useful for synchronizing multiple animations:

const flip = document.timeline.currentTime;

const anim1 = el1.animate(keyframes1, { duration: 300, delay: flip });
const anim2 = el2.animate(keyframes2, { duration: 300, delay: flip + 100 });
const anim3 = el3.animate(keyframes3, { duration: 300, delay: flip + 200 });

document.getAnimations() returns every active Animation object in the document — useful for pausing all animations at once:

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    document.getAnimations().forEach(anim => anim.pause());
  } else {
    document.getAnimations().forEach(anim => anim.play());
  }
});

Staggered Animations

A common pattern is staggering — each element in a list animates slightly after the previous one:

const items = document.querySelectorAll('.item');

items.forEach((item, index) => {
  item.animate([
    { opacity: 0, transform: 'translateY(20px)' },
    { opacity: 1, transform: 'translateY(0)' }
  ], {
    duration: 400,
    delay: index * 80,  // 80ms gap between each
    fill: 'both',
    easing: 'ease-out'
  });
});

KeyframeEffect for Reusable Animation Definitions

If you want to apply the same animation to multiple elements or reuse it, create a KeyframeEffect separately and pass it to Animation:

const fadeSlide = new KeyframeEffect(
  null,  // target set later
  [
    { opacity: 0, transform: 'translateY(10px)' },
    { opacity: 1, transform: 'translateY(0)' }
  ],
  { duration: 300, fill: 'both', easing: 'ease-out' }
);

el1.animate(fadeSlide);  // reuses the effect definition
el2.animate(fadeSlide);
el3.animate(fadeSlide);

The KeyframeEffect constructor signature is:

new KeyframeEffect(target, keyframes, options)

Pass null as target if you only want to define the effect without attaching it yet.

Performance Notes

WAAPI is hardware-accelerated for transform and opacity — these properties don’t trigger layout or paint. Keep your animations on these properties for the best performance:

// Good — runs on compositor
el.animate([
  { transform: 'translateX(0)' },
  { transform: 'translateX(100px)' }
], { duration: 300 });

// Bad — triggers layout and paint, janky
el.animate([
  { width: '100px' },
  { width: '300px' }
], { duration: 300 });

Animations on other properties (background-color, width, height, etc.) are more expensive — they go through the main thread.

See Also