The Web Animations API (WAAPI) brings CSS keyframe animation power to JavaScript: you can create, pause, reverse, and sequence animations entirely in script without managing CSS class names or listening for transitionend events. It is now baseline across all browsers and is the foundation that Framer Motion and GreenSock use under the hood.
Problem: CSS animations defined with @keyframes cannot be controlled programmatically — pausing, reversing, seeking to a specific frame, or reading the current animation state requires workarounds like toggling CSS classes.
Solution: Use the Web Animations API — call element.animate(keyframes, options) to create an Animation object with .play(), .pause(), .reverse(), and .currentTime properties. Listen for completion with animation.finished (a Promise). The API gives full programmatic control over animations that integrate with the browser's compositor for performance.
The examples below show basic element animation, chaining animations with Promise-based finished, a reusable fade-and-slide utility, and a WordPress admin notice that animates in on load and out on dismiss.
// ── 1. Basic WAAPI animation ─────────────────────────────────────────────
const box = document.querySelector( '.animated-box' );
const anim = box.animate(
[
{ opacity: 0, transform: 'translateY(20px)' }, // keyframe 0%
{ opacity: 1, transform: 'translateY(0)' }, // keyframe 100%
],
{
duration: 400, // ms
easing: 'ease-out',
fill: 'forwards', // keep final state after animation ends
delay: 100,
}
);
// Pause and resume
anim.pause();
anim.play();
// ── 2. Sequence animations with Promise ──────────────────────────────────
async function animateSequence( elements ) {
for ( const el of elements ) {
const a = el.animate(
[ { opacity: 0, translate: '0 1rem' }, { opacity: 1, translate: 'none' } ],
{ duration: 300, fill: 'forwards', easing: 'ease-out' }
);
await a.finished; // wait for this one before starting the next
}
}
animateSequence( document.querySelectorAll( '.stagger-item' ) );
// ── 3. Reusable fade-slide utility ───────────────────────────────────────
function fadeIn( el, { duration = 300, delay = 0 } = {} ) {
return el.animate(
[ { opacity: 0, translate: '0 12px' }, { opacity: 1, translate: 'none' } ],
{ duration, delay, fill: 'forwards', easing: 'cubic-bezier(.25,.8,.25,1)' }
).finished;
}
function fadeOut( el, { duration = 200 } = {} ) {
return el.animate(
[ { opacity: 1 }, { opacity: 0 } ],
{ duration, fill: 'forwards' }
).finished;
}
// ── 4. WordPress admin notice: animate in, animate out on dismiss ─────────
document.querySelectorAll( '.notice.is-animated' ).forEach( async notice => {
await fadeIn( notice, { duration: 350, delay: 80 } );
notice.querySelector( '.notice-dismiss' )?.addEventListener( 'click', async () => {
await fadeOut( notice );
notice.remove();
} );
} );
// ── 5. Reverse an animation (e.g. toggle open/close) ────────────────────
let isOpen = false;
const panel = document.querySelector( '.collapsible-panel' );
const panelAnim = panel.animate(
[ { maxHeight: '0px', opacity: 0 }, { maxHeight: '400px', opacity: 1 } ],
{ duration: 300, fill: 'both', easing: 'ease', paused: true }
);
panelAnim.pause(); // start paused
document.querySelector( '.toggle-btn' )?.addEventListener( 'click', () => {
isOpen = ! isOpen;
panelAnim.playbackRate = isOpen ? 1 : -1;
panelAnim.play();
} );
NOTE: Use fill: 'forwards' only when you intend to keep the final style — it creates a persistent "active effect" that overrides inline styles and can cause unexpected results if you later try to change the element's style with JavaScript; call animation.commitStyles() then animation.cancel() to bake the final state into inline styles and release the fill.