You’re probably using CSS animations or a heavy library like GSAP for your web animations. And look, I get it. CSS animations are easy, and GSAP is powerful. But there’s this thing sitting right in your browser that gives you the control of JavaScript with the performance of CSS, and it doesn’t cost you a single kilobyte of download.
It’s called the Web Animations API (WAAPI), and it’s criminally underused.
Working examples for you
Let me show you why you should care.
What’s Wrong with What We’re Doing Now?
CSS Animations Are Great, But…
CSS animations are declarative and performant. The browser optimizes them beautifully. But the moment you need dynamic control, like changing duration based on user input, syncing multiple animations, or responding to real-time events, you’re stuck.
You can’t easily:
- Pause and resume animations
- Reverse them mid-flight
- Get the current playback position
- Chain complex sequences dynamically
- Adjust timing on the fly
JavaScript Libraries Are Powerful, But…
GSAP, Anime.js, and similar libraries are incredible. But they come with a cost:
- Extra kilobytes to download (GSAP is ~50KB minified)
- Another dependency to maintain
- Learning curve for library-specific syntax
- Potential overkill for simple animations
The Web Animations API
WAAPI gives you JavaScript control with GPU-accelerated performance. It’s the native solution that combines the best of both worlds.
Think of it this way:
- CSS animations: Autopilot (great until you need manual control)
- JavaScript libraries: Commercial jetliner (powerful but heavy)
- WAAPI: Fighter jet (lightweight, fast, fully controllable)
The Basics: Your First Animation
Here’s the simplest WAAPI animation:
const box = document.querySelector('.box');
const animation = box.animate(
[
{ transform: 'translateX(0px)' },
{ transform: 'translateX(300px)' }
],
{
duration: 1000,
easing: 'ease-in-out',
fill: 'forwards'
}
);
That’s it. The box slides 300px to the right in one second. Notice the fill: 'forwards' option—this keeps the element in its final animated state instead of snapping back to the original position.
Let’s break it down:
-
First argument: Array of keyframes (like CSS
@keyframes) - Second argument: Timing options (duration, easing, delay, iterations, etc.)
The Same Thing in CSS
For comparison, here’s the CSS equivalent:
@keyframes slide {
from { transform: translateX(0px); }
to { transform: translateX(300px); }
}
.box {
animation: slide 1000ms ease-in-out;
}
Similar code, but WAAPI gives you a JavaScript handle to control it.
Animation Control
Here’s where WAAPI shines. You get back an Animation object you can control:
const animation = box.animate(
[
{ transform: 'scale(1)', opacity: 1 },
{ transform: 'scale(1.5)', opacity: 0 }
],
{ duration: 2000, easing: 'ease-out' }
);
// Pause it
animation.pause();// Resume it
animation.play();// Reverse it
animation.reverse();// Jump to a specific time (in milliseconds)
animation.currentTime = 1000;// Speed it up or slow it down
animation.playbackRate = 2; // 2x speed// Cancel it entirely
animation.cancel();
Try doing that cleanly with CSS animations. You can’t.
Interactive Card Flip
Let’s build something practical, a card that flips when you click it, with proper state management:
<div class="card" id="flip-card">Click to Flip</div>
const card = document.getElementById('flip-card');
let isFlipped = false;
let animation;
card.addEventListener('click', () => {
// Cancel previous animation if still running
if (animation) {
animation.cancel();
} // Define keyframes based on current state
const keyframes = isFlipped
? [
{ transform: 'rotateY(180deg)' },
{ transform: 'rotateY(0deg)' }
]
: [
{ transform: 'rotateY(0deg)' },
{ transform: 'rotateY(180deg)' }
]; // Animate
animation = card.animate(keyframes, {
duration: 600,
easing: 'ease-in-out',
fill: 'forwards' // Keep the final state
}); isFlipped = !isFlipped;
});
Important: Always cancel previous animations before starting new ones to prevent conflicts. This ensures smooth transitions even if users click rapidly.
Scroll-Linked Animation
Here’s something really cool: an element that fades in and scales up as you scroll.
const element = document.querySelector('.reveal-on-scroll');
function animateOnScroll() {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight;
// Calculate how much of the element is visible
const visible = Math.max(0, Math.min(1,
(windowHeight - rect.top) / windowHeight
)); element.animate(
[
{
opacity: visible,
transform: `scale(${0.8 + (visible * 0.2)})`
}
],
{
duration: 100,
fill: 'forwards'
}
);
}window.addEventListener('scroll', animateOnScroll);
animateOnScroll(); // Initial call
This creates a smooth reveal effect that’s perfectly synced to scroll position. Libraries charge you 30KB for this kind of functionality.
Sequencing Animations: The Promise Way
WAAPI animations return promises, which makes sequencing dead simple:
const box = document.querySelector('.box');
async function sequenceAnimation() {
// First: slide right
await box.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(200px)' }],
{ duration: 500, fill: 'forwards' }
).finished; // Then: slide down
await box.animate(
[{ transform: 'translateY(0)' }, { transform: 'translateY(200px)' }],
{ duration: 500, fill: 'forwards' }
).finished; // Finally: fade out
await box.animate(
[{ opacity: 1 }, { opacity: 0 }],
{ duration: 500, fill: 'forwards' }
).finished; console.log('Sequence complete!');
}sequenceAnimation();
The .finished property returns a promise that resolves when the animation completes. Clean, readable, no callbacks.
Performance Tricks
1. Use Transform and Opacity
These properties are GPU-accelerated. Stick to them when possible:
// Fast (GPU-accelerated)
element.animate([
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0.5 }
], { duration: 1000, fill: 'forwards' });
// Slower (causes layout recalculation)
element.animate([
{ left: '0px', width: '100px' },
{ left: '100px', width: '200px' }
], { duration: 1000, fill: 'forwards' });
Always include `` to maintain the final state after animation completes.
2. Use `` for Complex Animations
Tell the browser what’s about to animate:
.animated-element {
will-change: transform, opacity;
}
But remove it after the animation completes:
const anim = element.animate(keyframes, options);
anim.finished.then(() => {
element.style.willChange = 'auto';
});
3. Batch Animations
Start multiple animations in the same frame for better performance:
requestAnimationFrame(() => {
element1.animate(keyframes1, options1);
element2.animate(keyframes2, options2);
element3.animate(keyframes3, options3);
});
Practical Example: Loading Spinner
Here’s a smooth, customizable loading spinner with proper cleanup:
<div class="spinner"></div>
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
}
const spinner = document.querySelector('.spinner');
let spinnerAnimation;
// Start spinning
function startSpinner() {
if (spinnerAnimation) spinnerAnimation.cancel();
spinnerAnimation = spinner.animate(
[
{ transform: 'rotate(0deg)' },
{ transform: 'rotate(360deg)' }
],
{
duration: 1000,
iterations: Infinity,
easing: 'linear'
}
);
}// Stop and reset
function stopSpinner() {
if (spinnerAnimation) {
spinnerAnimation.cancel();
spinner.style.transform = 'rotate(0deg)';
}
}startSpinner();// Stop it when loading is done
setTimeout(stopSpinner, 5000);
Pro tip: Always store animation references so you can properly cancel them later. This prevents memory leaks and ensures clean state management.
Easing Functions: The Secret Sauce
WAAPI supports all CSS easing functions, plus custom cubic beziers:
// Built-in easings
{ easing: 'ease-in-out' }
{ easing: 'ease-in' }
{ easing: 'ease-out' }
{ easing: 'linear' }
// Custom cubic-bezier
{ easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)' } // Bounce effect// Steps (for sprite animations)
{ easing: 'steps(10, end)' }
Want a bounce effect? Use this:
element.animate(
[
{ transform: 'translateY(0)' },
{ transform: 'translateY(-100px)' }
],
{
duration: 600,
easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
fill: 'forwards'
}
);
Complex Example: Staggered List Animation
Animate a list of items with a stagger effect:
<ul class="list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
const items = document.querySelectorAll('.list li');
const staggerDelay = 100; // ms between each item
items.forEach((item, index) => {
item.animate(
[
{ opacity: 0, transform: 'translateX(-20px)' },
{ opacity: 1, transform: 'translateX(0)' }
],
{
duration: 400,
delay: index * staggerDelay,
fill: 'forwards',
easing: 'ease-out'
}
);
});
Each item fades in and slides from the left, with a 100ms delay between them. Smooth, professional, and no libraries.
Resetting Animations Smoothly
When you need to reset an animation, don’t just set CSS properties directly, animate back to the original state for a smooth transition:
function resetItems() {
items.forEach(item => {
item.animate(
[
{
opacity: window.getComputedStyle(item).opacity,
transform: window.getComputedStyle(item).transform
},
{ opacity: 0, transform: 'translateX(-20px)' }
],
{ duration: 300, fill: 'forwards' }
);
});
}
Using window.getComputedStyle() gets the current animated values, allowing you to smoothly transition from wherever the animation currently is.
Animation Events: Know When Things Happen
Listen for animation lifecycle events:
const animation = element.animate(keyframes, options);
// When animation finishes
animation.finished.then(() => {
console.log('Animation complete!');
});// When animation is cancelled
animation.finished.catch(() => {
console.log('Animation was cancelled');
});// Track playback changes
animation.onfinish = () => {
console.log('Finished!');
};animation.oncancel = () => {
console.log('Cancelled!');
};
Common Pitfalls and Solutions
Problem: Animations Jumping When Reset
Wrong way:
element.style.transform = 'translateX(0)'; // Jarring jump
Right way:
element.animate(
[
{ transform: window.getComputedStyle(element).transform },
{ transform: 'translateX(0)' }
],
{ duration: 300, fill: 'forwards' }
); // Smooth transition
Problem: Multiple Animations Conflicting
Solution: Always cancel previous animations before starting new ones:
let currentAnimation;
function animate() {
if (currentAnimation) currentAnimation.cancel();
currentAnimation = element.animate(keyframes, options);
}
Problem: Animation State Not Persisting
Solution: Always use fill: 'forwards':
element.animate(keyframes, {
duration: 1000,
fill: 'forwards' // This is crucial!
});
Browser Support
WAAPI has excellent browser support with 95%+ global coverage:
- Chrome/Edge: Full support from version 84+
- Firefox: Full support from version 75+
- Safari: Full support from version 13.1+
- Mobile: Full support on iOS Safari 13.4+, Chrome for Android, Firefox for Android
Earlier versions (Chrome 36–83, Firefox 47–74, Edge 79–83) had partial support. For legacy browser support, there’s a polyfill available, but if you’re targeting modern browsers (last 2–3 years), you don’t need it.
When to Use WAAPI vs. Alternatives
Use WAAPI when:
- You need programmatic control over animations
- You want CSS-level performance without CSS limitations
- You’re building interactive animations that respond to user input
- File size matters (no external dependencies)
- You need precise timing control
Stick with CSS when:
- Animations are purely decorative and static
- You don’t need JavaScript control
- Hover/focus states are sufficient
Use a library when:
- You need advanced features like morphing, path animations, or physics
- You’re doing extremely complex timeline management
- Team familiarity outweighs file size concerns
The Bottom Line
Web Animations API gives you the control of JavaScript with the performance of CSS, and it’s built right into your browser. No downloads, no dependencies, no compromises.
You can pause, reverse, speed up, slow down, and sequence animations with simple, readable code. You can sync animations to scroll, user input, or any other event. And it all runs buttery smooth because the browser optimizes it just like CSS animations.
If you’re reaching for a 50KB animation library for basic interactive animations, you’re probably overthinking it. WAAPI can handle 80% of what you need, and it’s already installed.
Give it a shot. Your bundle size will thank you, and your animations will run just as smooth, if not smoother.
Want to level up your web development skills?
This kind of content is what separates developers who ship fast, performant sites from those who rely on heavy frameworks for everything.
Don’t forget to bookmark this for reference, you’ll need it.
Did you learn something good today?
Then show some love. 👋
© Usman Writes
WordPress Developer | Website Strategist | SEO Specialist
Don’t forget to subscribe to Developer’s Journey to show your support.

Top comments (0)