JavaScript APIs

JavaScript Web Animations API: Powerful Browser Animations

Master the Web Animations API to create performant, complex animations with JavaScript. Learn keyframes, timing functions, animation control, and advanced techniques.

By JavaScript Document Team
animationsweb-apisperformanceinteractivegraphics

The Web Animations API provides a powerful way to create and control animations directly in JavaScript, offering the performance of CSS animations with the flexibility of JavaScript control.

Understanding Web Animations API

The Web Animations API unifies the animation features of CSS and SVG, allowing you to animate DOM elements using JavaScript with hardware acceleration.

Basic Animation Setup

// Simple animation
const element = document.querySelector('.box');

const animation = element.animate(
  [
    // Keyframes
    { transform: 'translateX(0px)' },
    { transform: 'translateX(300px)' },
  ],
  {
    // Timing options
    duration: 1000,
    iterations: 1,
    easing: 'ease-in-out',
  }
);

// Animation states
console.log(animation.playState); // 'running', 'paused', 'finished', etc.

// Control playback
animation.pause();
animation.play();
animation.reverse();
animation.finish();
animation.cancel();

// Animation properties
console.log(animation.currentTime); // Current position in ms
console.log(animation.playbackRate); // Speed multiplier
console.log(animation.effect); // The KeyframeEffect

// Events
animation.onfinish = () => console.log('Animation finished');
animation.oncancel = () => console.log('Animation cancelled');

Keyframe Animations

// Multiple keyframes
const fadeAndSlide = element.animate(
  [
    {
      opacity: 0,
      transform: 'translateY(-20px)',
      offset: 0, // Start
    },
    {
      opacity: 0.7,
      transform: 'translateY(-10px)',
      offset: 0.5, // Midpoint
    },
    {
      opacity: 1,
      transform: 'translateY(0)',
      offset: 1, // End
    },
  ],
  {
    duration: 500,
    easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
    fill: 'forwards', // Maintain final state
  }
);

// Property-based syntax
const colorAnimation = element.animate(
  {
    // Properties to animate
    backgroundColor: ['#ff0000', '#00ff00', '#0000ff'],
    transform: ['scale(1)', 'scale(1.2)', 'scale(1)'],
    borderRadius: ['0%', '50%', '0%'],
  },
  {
    duration: 2000,
    iterations: Infinity,
  }
);

// Complex keyframes with different easings
const complexAnimation = element.animate(
  [
    { transform: 'translateX(0) rotate(0deg)', easing: 'ease-out' },
    { transform: 'translateX(100px) rotate(180deg)', easing: 'ease-in-out' },
    { transform: 'translateX(200px) rotate(360deg)', easing: 'ease-in' },
    { transform: 'translateX(0) rotate(720deg)' },
  ],
  {
    duration: 3000,
    iterations: 1,
  }
);

Timing Options

// Complete timing options
const timingOptions = {
  // Duration in milliseconds
  duration: 1000,

  // Delay before starting
  delay: 500,

  // End delay
  endDelay: 200,

  // Number of iterations (Infinity for endless)
  iterations: 2,

  // Iteration start point (0 to 1)
  iterationStart: 0.5,

  // Animation direction
  direction: 'alternate', // 'normal', 'reverse', 'alternate', 'alternate-reverse'

  // Easing function
  easing: 'cubic-bezier(0.42, 0, 0.58, 1)',

  // Fill mode
  fill: 'both', // 'none', 'forwards', 'backwards', 'both', 'auto'

  // Playback rate
  playbackRate: 1,
};

const animation = element.animate(keyframes, timingOptions);

// Modify timing after creation
animation.updatePlaybackRate(2); // Double speed
animation.effect.updateTiming({ duration: 2000 }); // Change duration

Practical Applications

Animation Controller

class AnimationController {
  constructor(element) {
    this.element = element;
    this.animations = new Map();
    this.sequences = new Map();
  }

  // Create named animation
  create(name, keyframes, options) {
    const animation = this.element.animate(keyframes, {
      ...options,
      fill: 'both',
    });

    animation.pause(); // Start paused
    this.animations.set(name, animation);

    return animation;
  }

  // Play animation by name
  play(name, options = {}) {
    const animation = this.animations.get(name);
    if (!animation) {
      console.error(`Animation "${name}" not found`);
      return;
    }

    // Reset if needed
    if (options.reset) {
      animation.currentTime = 0;
    }

    // Update playback rate
    if (options.speed !== undefined) {
      animation.updatePlaybackRate(options.speed);
    }

    // Play
    animation.play();

    return animation.finished;
  }

  // Create animation sequence
  sequence(name, animations) {
    const sequence = animations.map(({ animationName, delay = 0 }) => ({
      animation: this.animations.get(animationName),
      delay,
    }));

    this.sequences.set(name, sequence);
  }

  // Play sequence
  async playSequence(name) {
    const sequence = this.sequences.get(name);
    if (!sequence) {
      console.error(`Sequence "${name}" not found`);
      return;
    }

    for (const { animation, delay } of sequence) {
      if (delay > 0) {
        await new Promise((resolve) => setTimeout(resolve, delay));
      }

      animation.currentTime = 0;
      animation.play();
      await animation.finished;
    }
  }

  // Pause all animations
  pauseAll() {
    this.animations.forEach((animation) => animation.pause());
  }

  // Resume all animations
  resumeAll() {
    this.animations.forEach((animation) => {
      if (animation.playState === 'paused') {
        animation.play();
      }
    });
  }

  // Get animation by name
  get(name) {
    return this.animations.get(name);
  }

  // Clean up
  destroy() {
    this.animations.forEach((animation) => animation.cancel());
    this.animations.clear();
    this.sequences.clear();
  }
}

// Usage
const controller = new AnimationController(element);

// Create animations
controller.create(
  'slideIn',
  [{ transform: 'translateX(-100%)' }, { transform: 'translateX(0)' }],
  { duration: 300, easing: 'ease-out' }
);

controller.create('fadeIn', [{ opacity: 0 }, { opacity: 1 }], {
  duration: 200,
});

controller.create(
  'bounce',
  [
    { transform: 'scale(1)' },
    { transform: 'scale(1.2)' },
    { transform: 'scale(1)' },
  ],
  { duration: 300, easing: 'ease-in-out' }
);

// Create sequence
controller.sequence('entrance', [
  { animationName: 'slideIn' },
  { animationName: 'fadeIn', delay: 100 },
  { animationName: 'bounce', delay: 50 },
]);

// Play sequence
controller.playSequence('entrance');

Interactive Animations

class InteractiveAnimation {
  constructor(element) {
    this.element = element;
    this.setupMouseTracking();
  }

  setupMouseTracking() {
    let animation = null;

    this.element.addEventListener('mouseenter', (e) => {
      const rect = this.element.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;

      animation = this.createRipple(x, y);
    });

    this.element.addEventListener('mouseleave', () => {
      if (animation) {
        animation.reverse();
      }
    });

    this.element.addEventListener('mousemove', (e) => {
      if (animation && animation.playState === 'running') {
        const rect = this.element.getBoundingClientRect();
        const x = (e.clientX - rect.left) / rect.width;
        const y = (e.clientY - rect.top) / rect.height;

        // Adjust animation based on mouse position
        animation.currentTime = animation.effect.getTiming().duration * x;
      }
    });
  }

  createRipple(x, y) {
    const ripple = document.createElement('div');
    ripple.className = 'ripple';
    ripple.style.left = `${x}px`;
    ripple.style.top = `${y}px`;

    this.element.appendChild(ripple);

    const animation = ripple.animate(
      [
        {
          transform: 'scale(0)',
          opacity: 1,
        },
        {
          transform: 'scale(4)',
          opacity: 0,
        },
      ],
      {
        duration: 600,
        easing: 'ease-out',
      }
    );

    animation.onfinish = () => ripple.remove();

    return animation;
  }
}

// Gesture-based animations
class GestureAnimations {
  constructor(element) {
    this.element = element;
    this.setupGestures();
  }

  setupGestures() {
    let startX = 0;
    let currentAnimation = null;

    this.element.addEventListener('touchstart', (e) => {
      startX = e.touches[0].clientX;
      if (currentAnimation) {
        currentAnimation.pause();
      }
    });

    this.element.addEventListener('touchmove', (e) => {
      const currentX = e.touches[0].clientX;
      const deltaX = currentX - startX;

      if (!currentAnimation) {
        currentAnimation = this.element.animate(
          [
            { transform: 'translateX(0)' },
            { transform: `translateX(${window.innerWidth}px)` },
          ],
          {
            duration: 1000,
            fill: 'both',
          }
        );
        currentAnimation.pause();
      }

      // Map swipe distance to animation progress
      const progress = Math.abs(deltaX) / window.innerWidth;
      currentAnimation.currentTime =
        progress * currentAnimation.effect.getTiming().duration;
    });

    this.element.addEventListener('touchend', (e) => {
      if (!currentAnimation) return;

      const endX = e.changedTouches[0].clientX;
      const deltaX = endX - startX;
      const threshold = window.innerWidth / 3;

      if (Math.abs(deltaX) > threshold) {
        // Complete the animation
        currentAnimation.playbackRate = deltaX > 0 ? 1 : -1;
        currentAnimation.play();
      } else {
        // Reverse back to start
        currentAnimation.reverse();
        currentAnimation.play();
      }

      currentAnimation = null;
    });
  }
}

Complex Animation Sequences

class AnimationTimeline {
  constructor() {
    this.tracks = new Map();
    this.globalTime = 0;
  }

  addTrack(name, element) {
    this.tracks.set(name, {
      element,
      animations: [],
    });
  }

  addAnimation(trackName, keyframes, options, startTime = 0) {
    const track = this.tracks.get(trackName);
    if (!track) {
      console.error(`Track "${trackName}" not found`);
      return;
    }

    const animation = track.element.animate(keyframes, {
      ...options,
      delay: startTime,
      fill: 'both',
    });

    animation.pause();

    track.animations.push({
      animation,
      startTime,
      duration: options.duration || 0,
    });

    return animation;
  }

  play() {
    // Find all unique start times
    const startTimes = new Set();
    this.tracks.forEach((track) => {
      track.animations.forEach(({ startTime }) => {
        startTimes.add(startTime);
      });
    });

    // Sort start times
    const timeline = Array.from(startTimes).sort((a, b) => a - b);

    // Play animations at their start times
    timeline.forEach((time, index) => {
      const delay = index === 0 ? time : time - timeline[index - 1];

      setTimeout(() => {
        this.tracks.forEach((track) => {
          track.animations.forEach(({ animation, startTime }) => {
            if (startTime === time) {
              animation.play();
            }
          });
        });
      }, delay);
    });
  }

  pause() {
    this.tracks.forEach((track) => {
      track.animations.forEach(({ animation }) => {
        animation.pause();
      });
    });
  }

  seek(time) {
    this.globalTime = time;

    this.tracks.forEach((track) => {
      track.animations.forEach(({ animation, startTime, duration }) => {
        if (time < startTime) {
          animation.currentTime = 0;
          animation.pause();
        } else if (time > startTime + duration) {
          animation.currentTime = duration;
          animation.pause();
        } else {
          animation.currentTime = time - startTime;
          animation.pause();
        }
      });
    });
  }

  reverse() {
    this.tracks.forEach((track) => {
      track.animations.forEach(({ animation }) => {
        animation.reverse();
        animation.play();
      });
    });
  }
}

// Usage
const timeline = new AnimationTimeline();

// Add tracks
timeline.addTrack('header', document.querySelector('.header'));
timeline.addTrack('content', document.querySelector('.content'));
timeline.addTrack('footer', document.querySelector('.footer'));

// Add animations
timeline.addAnimation(
  'header',
  [
    { opacity: 0, transform: 'translateY(-50px)' },
    { opacity: 1, transform: 'translateY(0)' },
  ],
  { duration: 500, easing: 'ease-out' },
  0
);

timeline.addAnimation(
  'content',
  [
    { opacity: 0, transform: 'scale(0.9)' },
    { opacity: 1, transform: 'scale(1)' },
  ],
  { duration: 600, easing: 'ease-out' },
  200
);

timeline.addAnimation(
  'footer',
  [
    { opacity: 0, transform: 'translateY(50px)' },
    { opacity: 1, transform: 'translateY(0)' },
  ],
  { duration: 500, easing: 'ease-out' },
  400
);

// Play timeline
timeline.play();

Morphing Animations

class MorphAnimation {
  constructor(element) {
    this.element = element;
    this.shapes = new Map();
  }

  addShape(name, path) {
    this.shapes.set(name, path);
  }

  morph(fromShape, toShape, options = {}) {
    const fromPath = this.shapes.get(fromShape);
    const toPath = this.shapes.get(toShape);

    if (!fromPath || !toPath) {
      console.error('Shape not found');
      return;
    }

    const animation = this.element.animate([{ d: fromPath }, { d: toPath }], {
      duration: options.duration || 1000,
      easing: options.easing || 'ease-in-out',
      fill: 'forwards',
    });

    return animation;
  }

  createMorphSequence(shapes, options = {}) {
    const keyframes = shapes.map((shapeName) => ({
      d: this.shapes.get(shapeName),
    }));

    return this.element.animate(keyframes, {
      duration: options.duration || shapes.length * 1000,
      easing: options.easing || 'ease-in-out',
      iterations: options.iterations || 1,
      direction: options.direction || 'normal',
    });
  }
}

// Path morphing example
const morpher = new MorphAnimation(document.querySelector('#morphPath'));

morpher.addShape(
  'circle',
  'M 50,50 m -25,0 a 25,25 0 1,1 50,0 a 25,25 0 1,1 -50,0'
);
morpher.addShape('square', 'M 25,25 L 75,25 L 75,75 L 25,75 Z');
morpher.addShape('triangle', 'M 50,25 L 75,75 L 25,75 Z');
morpher.addShape(
  'star',
  'M 50,25 L 60,45 L 80,45 L 65,60 L 75,80 L 50,65 L 25,80 L 35,60 L 20,45 L 40,45 Z'
);

// Create morph sequence
const morphSequence = morpher.createMorphSequence(
  ['circle', 'square', 'triangle', 'star', 'circle'],
  { duration: 4000, iterations: Infinity }
);

Performance Monitoring

class AnimationPerformanceMonitor {
  constructor() {
    this.metrics = new Map();
    this.threshold = 16.67; // 60 FPS threshold
  }

  monitor(animation, name) {
    const startTime = performance.now();
    let lastFrameTime = startTime;
    let frameCount = 0;
    let droppedFrames = 0;

    const checkFrame = () => {
      if (animation.playState !== 'running') {
        this.recordMetrics(name, {
          duration: performance.now() - startTime,
          frameCount,
          droppedFrames,
          averageFPS: frameCount / ((performance.now() - startTime) / 1000),
        });
        return;
      }

      const currentTime = performance.now();
      const deltaTime = currentTime - lastFrameTime;

      if (deltaTime > this.threshold) {
        droppedFrames++;
      }

      frameCount++;
      lastFrameTime = currentTime;

      requestAnimationFrame(checkFrame);
    };

    requestAnimationFrame(checkFrame);
  }

  recordMetrics(name, metrics) {
    this.metrics.set(name, metrics);

    if (metrics.droppedFrames > 0) {
      console.warn(
        `Animation "${name}" dropped ${metrics.droppedFrames} frames`
      );
    }
  }

  getReport() {
    const report = {};

    this.metrics.forEach((metrics, name) => {
      report[name] = {
        ...metrics,
        performance:
          metrics.averageFPS >= 55
            ? 'Good'
            : metrics.averageFPS >= 30
              ? 'Fair'
              : 'Poor',
      };
    });

    return report;
  }
}

// Optimized animations
class OptimizedAnimations {
  static willChangeAnimation(element, properties) {
    // Set will-change for optimization
    element.style.willChange = properties.join(', ');

    const animation = element.animate(...arguments);

    animation.onfinish = () => {
      element.style.willChange = 'auto';
    };

    return animation;
  }

  static batchAnimate(elements, keyframes, options) {
    // Use a single animation for multiple elements
    const animations = [];

    // Request animation frame for batching
    requestAnimationFrame(() => {
      elements.forEach((element) => {
        const animation = element.animate(keyframes, options);
        animations.push(animation);
      });
    });

    return animations;
  }

  static throttledAnimation(element, keyframes, options, throttleMs = 100) {
    let lastTime = 0;
    let animation = null;

    return function () {
      const now = Date.now();

      if (now - lastTime >= throttleMs) {
        if (animation) {
          animation.cancel();
        }

        animation = element.animate(keyframes, options);
        lastTime = now;
      }

      return animation;
    };
  }
}

Responsive Animations

class ResponsiveAnimations {
  constructor() {
    this.breakpoints = {
      mobile: 768,
      tablet: 1024,
      desktop: 1440,
    };

    this.animations = new Map();
    this.currentBreakpoint = this.getBreakpoint();

    this.setupResizeListener();
  }

  getBreakpoint() {
    const width = window.innerWidth;

    if (width < this.breakpoints.mobile) return 'mobile';
    if (width < this.breakpoints.tablet) return 'tablet';
    if (width < this.breakpoints.desktop) return 'desktop';
    return 'large';
  }

  registerAnimation(name, element, variants) {
    this.animations.set(name, { element, variants });
  }

  play(name) {
    const animation = this.animations.get(name);
    if (!animation) return;

    const variant = animation.variants[this.currentBreakpoint];
    if (!variant) return;

    return animation.element.animate(variant.keyframes, variant.options);
  }

  setupResizeListener() {
    let resizeTimeout;

    window.addEventListener('resize', () => {
      clearTimeout(resizeTimeout);

      resizeTimeout = setTimeout(() => {
        const newBreakpoint = this.getBreakpoint();

        if (newBreakpoint !== this.currentBreakpoint) {
          this.currentBreakpoint = newBreakpoint;
          this.updateAnimations();
        }
      }, 250);
    });
  }

  updateAnimations() {
    // Cancel and restart active animations with new parameters
    this.animations.forEach((animation, name) => {
      const element = animation.element;
      const activeAnimations = element.getAnimations();

      activeAnimations.forEach((anim) => {
        const currentTime = anim.currentTime;
        anim.cancel();

        // Restart with new variant
        const newAnim = this.play(name);
        if (newAnim) {
          newAnim.currentTime = currentTime;
        }
      });
    });
  }
}

// Usage
const responsiveAnims = new ResponsiveAnimations();

responsiveAnims.registerAnimation('hero', heroElement, {
  mobile: {
    keyframes: [
      { transform: 'scale(0.8)', opacity: 0 },
      { transform: 'scale(1)', opacity: 1 },
    ],
    options: { duration: 300, easing: 'ease-out' },
  },
  tablet: {
    keyframes: [
      { transform: 'translateX(-50px)', opacity: 0 },
      { transform: 'translateX(0)', opacity: 1 },
    ],
    options: { duration: 500, easing: 'ease-out' },
  },
  desktop: {
    keyframes: [
      { transform: 'translateY(-100px) scale(1.2)', opacity: 0 },
      { transform: 'translateY(0) scale(1)', opacity: 1 },
    ],
    options: { duration: 800, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' },
  },
});

Best Practices

  1. Check for API support
function isWebAnimationsSupported() {
  return 'animate' in Element.prototype;
}

// Polyfill if needed
if (!isWebAnimationsSupported()) {
  // Load web-animations-js polyfill
  const script = document.createElement('script');
  script.src =
    'https://unpkg.com/web-animations-js@2.3.2/web-animations.min.js';
  document.head.appendChild(script);
}
  1. Optimize for performance
// Use transform and opacity for best performance
const performantAnimation = element.animate(
  [
    { transform: 'translateX(0) scale(1)', opacity: 1 },
    { transform: 'translateX(100px) scale(1.1)', opacity: 0.8 },
  ],
  {
    duration: 500,
    easing: 'ease-out',
  }
);

// Avoid animating layout properties
// Bad: animating width, height, top, left
// Good: animating transform, opacity
  1. Clean up animations
class AnimationManager {
  constructor() {
    this.activeAnimations = new Set();
  }

  create(element, keyframes, options) {
    const animation = element.animate(keyframes, options);
    this.activeAnimations.add(animation);

    animation.onfinish = () => {
      this.activeAnimations.delete(animation);
    };

    animation.oncancel = () => {
      this.activeAnimations.delete(animation);
    };

    return animation;
  }

  cleanup() {
    this.activeAnimations.forEach((animation) => {
      animation.cancel();
    });
    this.activeAnimations.clear();
  }
}
  1. Handle animation promises
async function sequentialAnimations() {
  try {
    await element1.animate(keyframes1, options1).finished;
    await element2.animate(keyframes2, options2).finished;
    await element3.animate(keyframes3, options3).finished;

    console.log('All animations completed');
  } catch (error) {
    console.error('Animation interrupted:', error);
  }
}

// Parallel animations
async function parallelAnimations() {
  const animations = [
    element1.animate(keyframes1, options1).finished,
    element2.animate(keyframes2, options2).finished,
    element3.animate(keyframes3, options3).finished,
  ];

  try {
    await Promise.all(animations);
    console.log('All animations completed');
  } catch (error) {
    console.error('One or more animations failed:', error);
  }
}

Conclusion

The Web Animations API provides a powerful, performant way to create complex animations with JavaScript. It combines the performance benefits of CSS animations with the flexibility and control of JavaScript, making it ideal for interactive experiences, complex sequences, and dynamic animations. By understanding keyframes, timing functions, and animation control methods, you can create engaging, smooth animations that enhance user experience while maintaining optimal performance.