JavaScript Web Animations API Table
| Piece | What it does | Field note |
|---|---|---|
el.animate(keyframes, opts) | CSS animation from JS | Same keyframes/easing - programmable start, runtime values |
anim.finished | Awaitable end | A promise - chain logic without animationend listeners |
anim.playbackRate | Speed control | Negative = reverse - the scrubber/undo animation trick |
anim.currentTime | Random access | Seek the animation - CSS transitions cannot do this |
KeyframeEffect | Reusable keyframes | The keyframe object separate from the animation |
anim.commitStyles() | Bake the end state | Writes final styles to the element - un-animates cleanly |
persist / replaceable | Keep replaced anims | Fill modes auto-replace - persist opts out |
vs CSS animations | Runtime values decide | CSS: known at authoring; WAAPI: values known at RUNTIME |
The Web Animations API is CSS animation with a PROGRAMMATIC handle: el.animate(keyframes, options) runs the same keyframes and easing from JavaScript - which unlocks what stylesheets cannot express: runtime values, random access, speed control and awaitable ends.
Bottom line: CSS animations for what is known at authoring time; WAAPI for what is known at RUNTIME - animate to a measured height, from a clicked position, at a speed the user set. anim.finished replaces animationend listeners with await; commitStyles() bakes the final frame into real styles.
The honest part: WAAPI animations still obey the performance rules - transform and opacity stay compositor-friendly, layout properties reflow per frame. The API gives control over the animation, not exemption from its cost.
How to use
- Animate to runtime values: el.animate([{ transform: 'scale(1)' }, { transform: `scale(${target / el.offsetWidth})` }], 300) - measured targets, no pre-authored keyframes.
- Await the end: await el.animate(...).finished - sequential choreography reads top-to-bottom instead of nesting animationend listeners.
- Scrub and reverse: anim.currentTime = 500 seeks; anim.playbackRate = -1 plays backward - undo animations and scrubbers without duplicate keyframes.
Frequently asked questions
When does WAAPI beat plain CSS animations?
When the animation's PARAMETERS are runtime facts. CSS keyframes are authored: 'slide in 300ms' is fixed in the stylesheet. WAAPI takes the same keyframe format from JavaScript, so the distance, duration or easing can come from measurements, user settings or application state - animating a card from its clicked position to the detail view is pure WAAPI (FLIP made trivial) and impossible in pure CSS. The second win is control: pause, seek, reverse, change playbackRate mid-flight - CSS animations expose only animation-play-state. The rule of thumb: decorative loops and hover states stay CSS; interactive, data-driven or choreographed motion goes WAAPI.
Why is anim.finished better than the animationend event?
Control flow. The event form nests: add listener, set flags, remember to remove the listener, handle the never-fires edge (element removed mid-animation). finished is a PROMISE: await anim.finished reads as a step in sequential choreography - run animation, await, start the next - and rejection (a cancelled animation) flows through normal try/catch. It also composes: Promise.all([a.finished, b.finished]) waits for parallel animations, which event code renders as counter callbacks. The event API remains for fire-and-forget cases where nothing needs to await the end.
What do commitStyles() and persist solve?
The disappearing-end-state problem. By default, an animation's styles exist only DURING the animation - after it ends (fill: none) the element snaps back to its stylesheet state. fill: forwards keeps the animated values but stacks effects that never release (a memory and cascade-growing leak across many animations). commitStyles() writes the animation's CURRENT computed values into the element's inline style and ends the animation - the visual end state becomes real CSS, the animation object goes away cleanly. persist() is the opt-out from auto-replacement for animations you intend to resume. Together: animations that transition an element into its new permanent state without residue.
How do WAAPI and IntersectionObserver compose in real products?
Trigger-on-visibility choreography. The observer says WHEN (the card entered the viewport); WAAPI says WHAT and HOW (slide up, stagger children). The pattern: observe cards, on intersection fire el.animate with a delay proportional to the card's index (the stagger), unobserve - one-time entrance animations with no scroll listeners and no CSS classes toggled. The composition beats pure CSS (which cannot know visibility) and pure JS (which would hand-animate transforms). It also inherits the performance rules: the keyframes should animate transform and opacity so the entrance runs on the compositor, per the transform table's guidance.