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.
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
- 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);
}
- 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
- 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();
}
}
- 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.