Native CSS Animations & the WAAPI
Declarative @keyframes vs imperative element.animate().
There are two ways to animate natively in the browser, and they share one engine. The declarative path — @keyframes and transition — hands
the whole timeline to the browser. The imperative path — element.animate(), the Web Animations API — hands you a live Animation object you can play, pause, seek and reverse. Every JS animation library
you'll meet later is, underneath, orchestrating one of these two.
Declarative vs imperative
With @keyframes you describe the destination states and the browser interpolates
forever. With a transition you describe nothing but the easing and let a state change (a class toggle, a hover) trigger an implicit tween from the old value to
the new one. Both are set-and-forget — great until you need to interrupt them.
The Web Animations API (WAAPI) is the imperative counterpart. element.animate(keyframes, options) kicks off the same kind of animation but
returns an Animation object — a handle onto a running timeline.
Same animation, three ways
Below is one visual — a box translating, rotating a full turn, and fading — authored three
ways. Flip between the techniques to read each one's code. The live box on the right is the real WAAPI version: the scrubber is wired straight to the Animation's currentTime, and the buttons call .play() / .pause() / .reverse() on it.
currentTime = 0ms / 2400ms (0%) paused// Imperative: animate() returns a first-class Animation object.
const anim = box.animate(
[
{ transform: 'translateX(-110px) rotate(0deg) scale(1)', opacity: .25, offset: 0 },
{ transform: 'translateX(0) rotate(180deg) scale(1.35)', opacity: 1, offset: .5 },
{ transform: 'translateX(110px) rotate(360deg) scale(1)', opacity: .25, offset: 1 }
],
{ duration: 2400, easing: 'ease-in-out', fill: 'both' }
);
// You hold the timeline. Grab and redirect it any time:
anim.pause();
anim.currentTime = 1200; // seek (drives the scrubber →)
anim.playbackRate = 2; // half-speed … 2×
anim.reverse(); // flip direction, keep momentum
await anim.finished; // a Promise
box.getAnimations(); // [anim] — enumerate live animationsAll three produce the same motion. Only WAAPI hands you the live object the scrubber and buttons are driving.
Composite operations: replace vs add
Two animations can target the same property at once. By default the later one 'replace's the earlier — the last writer wins. With composite: 'add' their values are summed, letting you layer
independent motions (a constant drift plus a reactive nudge) without one clobbering the other. 'accumulate' is a third mode that adds repeating effects across iterations.
composite: 'replace' The bob overwrites the sway — only the last animation's translate wins, so the box just moves up & down.
composite: 'add' The bob is summed with the sway — both effects combine into a looping figure-eight-ish path.
Core element.animate() and playback control are Baseline across all engines. composite:'add'/'accumulate' shipped a bit later but is now broadly supported; getAnimations() is everywhere modern.
Playground: a WAAPI sandbox
element.animate() is built into the browser — no imports. Edit the keyframes array
and the options object below and watch them apply to a live box. Try changing iterations to Infinity, swapping the easing for a cubic-bezier(...), or adding an opacity channel to a keyframe.