Skip to content
Web animation enginesLesson 4 of 4Intermediate12 min

The Web Animations API

element.animate() runs keyframe animations from JavaScript on the compositor — with play, pause, reverse, and a finished promise CSS can't give you.

Kept on this device only.

In one line: the Web Animations API lets you run the same keyframe animations CSS does, but from JavaScript — so you get play, pause, reverse, dynamic values, and a finished promise, while the animation still runs on the browser's compositor.

What it is

CSS keyframes are declarative and fast, but they're hard to control from code: you toggle a class and hope, and coordinating a sequence or reacting to "is it done?" means listening for animationend and bookkeeping.

The Web Animations API (WAAPI) is the same animation engine exposed to JavaScript. element.animate(keyframes, options) starts an animation and hands back an Animation object you can pause(), play(), reverse(), scrub via currentTime, and await through its finished promise. The keyframes are plain objects; the options are duration, easing, delay, iterations, and fill.

Why it matters

It hits a sweet spot between CSS transitions (cheap but rigid) and a full animation library (powerful but a dependency). For dynamic, interruptible motion — a sheet you can fling and catch mid-flight, a sequence that depends on runtime values, an animation you must await before navigating — WAAPI gives you imperative control without shipping a library.

Crucially it stays on the compositor for transform/opacity, so unlike animating via requestAnimationFrame and writing inline styles every frame (which runs on the main thread and competes with your app), WAAPI animations are offloaded and stay smooth under load.

See it

Live demo
Tweak it2
600
Play the move, then change easing and duration — the generated element.animate() call updates.

Hit Play, then change the easing and duration — the generated element.animate() call below updates to match what's running. The spring easing overshoots and settles; linear marches; ease glides.

How it works

animate() takes two arguments:

  • Keyframes — an array of style states ([{ transform: "translateX(0)" }, { transform: "translateX(11rem)" }]), or a single object for an implicit from/to. Stick to transform and opacity for compositor performance.
  • Options{ duration, easing, delay, iterations, fill }. easing accepts the same functions as CSS ("ease", cubic-bezier(...), "steps()"); fill: "forwards" persists the end state instead of snapping back.

The returned Animation is the control surface. await anim.finished resolves when it ends; anim.reverse() plays it backwards from the current point (great for hover in/out and open/close); anim.cancel() removes it. Because the browser owns the timeline, all of this stays in sync with the compositor.

Build it

TypeScript
A reusable, awaitable slide-and-fade with a reduced-motion fallback
type Options = { duration?: number };
 
export function slideIn(el: HTMLElement, { duration = 320 }: Options = {}): Animation {
  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
 
  const keyframes: Keyframe[] = reduce
    ? [{ opacity: 0 }, { opacity: 1 }] // honour the preference: fade only
    : [
        { transform: "translateY(12px)", opacity: 0 },
        { transform: "translateY(0)", opacity: 1 },
      ];
 
  // Returns the Animation so callers can pause / reverse / await it.
  return el.animate(keyframes, {
    duration: reduce ? 120 : duration,
    easing: "cubic-bezier(0.2, 0.7, 0.2, 1)",
    fill: "forwards",
  });
}
 
// await it, then do the next thing — no animationend bookkeeping.
async function reveal(el: HTMLElement) {
  await slideIn(el).finished;
  el.focus();
}

Make it yours

Use the controls beside the demo above to change easing and duration — each change updates the example live.

Experiment in the playground
  • Switch to spring easing and watch the overshoot — that's a cubic-bezier whose curve passes above 1 before settling.
  • Stretch the duration and the same call simply runs longer; the keyframes don't change, only the timing option.
  • Picture calling .reverse() on the returned Animation for the exit — one object drives both directions.

Reproduce it with an LLM

Reproduce it with an LLM

You are a senior front-end engineer. Use the Web Animations API (`element.animate(keyframes, options)`) to build a reusable function that slides and fades a notification element in, holds it, then slides it out — returning the Animation object so the caller can pause, reverse, or await its `finished` promise. Animate only transform and opacity so it stays on the compositor; set `fill: "forwards"` to persist the end state; and read `matchMedia('(prefers-reduced-motion: reduce)')` to fall back to an instant opacity change. Explain in a comment why WAAPI is chosen over CSS keyframes here (dynamic values + JS control). Return only the code with imports.

Pitfalls & accessibility

Further reading