Reference guide for the browser's 3 native animation APIs. Complements the skill view-transitions (navigation between states/pages — see ../view-transitions/SKILL.md).
When to Use Each API
| Need | API | Why |
|---|---|---|
| Hover, focus, simple state | CSS Transitions | Declarative, zero JS |
| Pure CSS keyframe sequence | CSS Animations | @keyframes + animation |
| Runtime control (pause, reverse, seek) | WAAPI | Element.animate() returns Animation controllable |
| Scroll-driven reveal/parallax | CSS animation-timeline or WAAPI ScrollTimeline |
Off main thread, no jank |
Entry from display:none |
CSS Transitions + @starting-style + allow-discrete |
Native, no JS |
Animation of height: auto |
CSS + interpolate-size |
Chrome 129+, progressive enhancement |
| Per-frame logic (physics, canvas) | requestAnimationFrame |
Last resort, main thread only |
Web Animations API (WAAPI)
Element.animate()
const anim = element.animate(
[{ transform: 'translateX(0)', opacity: 1 }, { transform: 'translateX(300px)', opacity: 0 }],
{
duration: 300, // milisegundos (NO segundos como CSS)
easing: 'ease-out', // default es 'linear' (distinto a CSS!)
iterations: 1, // Infinity para loops
fill: 'forwards', // none | forwards | backwards | both
direction: 'alternate', // normal | reverse | alternate | alternate-reverse
delay: 100,
composite: 'replace', // replace | add | accumulate
}
);
Keyframes: array of objects (flexible, supports offset/easing per keyframe) or compact object ({ opacity: [0, 1] }). Implicit from/to: element.animate({ transform: 'translateX(300px)' }, 400) animates from the current value. Always camelCase (backgroundColor).
The Animation Object
// Playback
anim.play(); anim.pause(); anim.reverse(); anim.finish(); anim.cancel();
// Estado
anim.playState; // 'idle' | 'running' | 'paused' | 'finished'
anim.currentTime; // ms | null
anim.playbackRate; // 1.0 default, negativo = reversa
anim.updatePlaybackRate(0.5); // smooth rate change
// Promises (async/await)
await anim.ready; // animacion arranco
await anim.finished; // animacion termino
// Events: onfinish, oncancel, onremove
Persist Styles (Prefer over fill:'forwards')
anim.finished.then(() => { anim.commitStyles(); anim.cancel(); });
fill: 'forwards' keeps memory and has side-effects on the cascade. commitStyles() writes the computed values to element.style and frees resources.
getAnimations()
element.getAnimations(); // del elemento (incluye CSS animations/transitions)
document.getAnimations(); // todas del documento
For advanced features (KeyframeEffect constructor, compositing, pseudo-element targeting), read references/waapi-advanced.md.
CSS Transitions
transition: opacity 200ms ease-out, transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
@starting-style — Entry Animations
Without @starting-style, transitions do not trigger on the first render (there is no "before" state):
.card {
opacity: 1; transform: translateY(0);
transition: opacity 300ms, transform 300ms;
@starting-style { opacity: 0; transform: translateY(8px); }
}
Animate display with allow-discrete
.dialog {
display: none; opacity: 0;
transition: opacity 200ms ease, display 200ms allow-discrete;
}
.dialog[open] { display: block; opacity: 1; }
@starting-style { .dialog[open] { opacity: 0; } }
Events: transitionend, transitionrun, transitionstart, transitioncancel. Gotcha: transitionend does NOT fire if interrupted.
CSS Animations
@keyframes slide-in {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.element {
animation: slide-in 300ms ease-out forwards;
/* name | duration | easing | fill-mode */
/* Composicion con otras animaciones */
animation-composition: add; /* replace | add | accumulate */
}
Events
element.addEventListener('animationend', (e: AnimationEvent) => {
e.animationName; // nombre del @keyframes
});
// Tambien: animationstart, animationcancel, animationiteration
Performance
Compositor properties (GPU, always prefer): transform, opacity, filter, clip-path
Main thread properties (avoid animating): width, height, top, left, background-color, box-shadow
CSS Animations and WAAPI run on the compositor for GPU properties. requestAnimationFrame always runs on the main thread.
will-change: apply only when the animation is imminent (e.g.: .card:hover { will-change: transform; }). Do not apply to everything — every promoted layer consumes GPU memory.
Layout thrashing: do not interleave reads (offsetHeight) and writes (style.height) in a loop. Batch reads first, then writes.
Scroll-Driven Animations
For scroll-driven animations (ScrollTimeline, ViewTimeline, animation-timeline, animation-range), read references/scroll-driven.md.
Modern Patterns
For interpolate-size, prefers-reduced-motion, @supports guards and other modern patterns, read references/patterns.md.
Gotchas
- WAAPI default easing is
linear, noteaselike CSS — always specify - WAAPI duration in milliseconds, CSS in seconds —
duration: 300= 300ms fill: 'forwards'has side-effects — prefercommitStyles()+cancel()- Transitions don't trigger without a previous state — use
@starting-styleor rAF delay transitionenddoesn't fire if interrupted — don't rely on it as the only cleanup- Compositor vs main thread — animate only
transform,opacity,filter,clip-path will-changeisn't free — each layer consumes GPU memoryanimation-duration: autorequired for scroll-driven timelines in CSS
Browser Support (2025)
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
Element.animate() core |
36+ | 79+ | 48+ | 13.1+ |
commitStyles() / persist() |
84+ | 84+ | 75+ | 13.1+ |
@starting-style |
117+ | 117+ | 129+ | 17.5+ |
transition-behavior: allow-discrete |
117+ | 117+ | 129+ | 17.5+ |
| Scroll-driven (CSS) | 115+ | 115+ | flag | 18+ (partial) |
ScrollTimeline / ViewTimeline JS |
115+ | 115+ | flag | 18+ (partial) |
interpolate-size |
129+ | 129+ | No | No |
Pre-Deploy Checklist
[ ] prefers-reduced-motion implementado
[ ] Solo animar propiedades de compositor (transform, opacity, filter)
[ ] will-change aplicado selectivamente y removido post-animacion
[ ] Duraciones razonables (150-400ms transiciones, hasta 1s animaciones complejas)
[ ] @starting-style para entry animations
[ ] commitStyles() + cancel() en vez de fill:'forwards' persistente
[ ] Scroll-driven animations con @supports guard y fallback visible
[ ] Testeado en Chrome, Safari Y Firefox