DEV Community

Cover image for How to Animate Like a Pro with Web Animations API (No FW Needed)
Muhammad Usman
Muhammad Usman

Posted on • Originally published at javascript.plainenglish.io

How to Animate Like a Pro with Web Animations API (No FW Needed)

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');
Enter fullscreen mode Exit fullscreen mode
const animation = box.animate(
  [
    { transform: 'translateX(0px)' },
    { transform: 'translateX(300px)' }
  ],
  {
    duration: 1000,
    easing: 'ease-in-out',
    fill: 'forwards'
  }
);
Enter fullscreen mode Exit fullscreen mode

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); }
}
Enter fullscreen mode Exit fullscreen mode
.box {
  animation: slide 1000ms ease-in-out;
}
Enter fullscreen mode Exit fullscreen mode

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' }
);
Enter fullscreen mode Exit fullscreen mode
// 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();
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
const card = document.getElementById('flip-card');
let isFlipped = false;
let animation;
Enter fullscreen mode Exit fullscreen mode
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;
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode

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' });
Enter fullscreen mode Exit fullscreen mode
// Slower (causes layout recalculation)
element.animate([
  { left: '0px', width: '100px' },
  { left: '100px', width: '200px' }
], { duration: 1000, fill: 'forwards' });
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

But remove it after the animation completes:

const anim = element.animate(keyframes, options);
anim.finished.then(() => {
  element.style.willChange = 'auto';
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

Practical Example: Loading Spinner

Here’s a smooth, customizable loading spinner with proper cleanup:

<div class="spinner"></div>
Enter fullscreen mode Exit fullscreen mode
.spinner {
  width: 50px;
  height: 50px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
}
Enter fullscreen mode Exit fullscreen mode
const spinner = document.querySelector('.spinner');
let spinnerAnimation;
Enter fullscreen mode Exit fullscreen mode
// 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);
Enter fullscreen mode Exit fullscreen mode

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' }
Enter fullscreen mode Exit fullscreen mode
// Custom cubic-bezier
{ easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)' } // Bounce effect// Steps (for sprite animations)
{ easing: 'steps(10, end)' }
Enter fullscreen mode Exit fullscreen mode

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'
  }
);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
const items = document.querySelectorAll('.list li');
const staggerDelay = 100; // ms between each item
Enter fullscreen mode Exit fullscreen mode
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'
    }
  );
});
Enter fullscreen mode Exit fullscreen mode

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' }
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
// 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!');
};
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls and Solutions

Problem: Animations Jumping When Reset

Wrong way:

element.style.transform = 'translateX(0)'; // Jarring jump
Enter fullscreen mode Exit fullscreen mode

Right way:

element.animate(
  [
    { transform: window.getComputedStyle(element).transform },
    { transform: 'translateX(0)' }
  ],
  { duration: 300, fill: 'forwards' }
); // Smooth transition
Enter fullscreen mode Exit fullscreen mode

Problem: Multiple Animations Conflicting

Solution: Always cancel previous animations before starting new ones:

let currentAnimation;
Enter fullscreen mode Exit fullscreen mode
function animate() {
  if (currentAnimation) currentAnimation.cancel();
  currentAnimation = element.animate(keyframes, options);
}
Enter fullscreen mode Exit fullscreen mode

Problem: Animation State Not Persisting

Solution: Always use fill: 'forwards':

element.animate(keyframes, {
  duration: 1000,
  fill: 'forwards' // This is crucial!
});
Enter fullscreen mode Exit fullscreen mode

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.
Developer's Journey

Top comments (0)