Skip to content
Web Animations Guide — CSS Transitions, Web Animations API, and Canvas

Web Animations Guide — CSS Transitions, Web Animations API, and Canvas

DodaTech Updated Jun 20, 2026 8 min read

Web animations bring interfaces to life — guiding user attention, providing feedback, telling stories, and creating memorable experiences through motion design on the web.

What You’ll Learn

  • CSS transitions and keyframe animations
  • The Web Animations API for JavaScript-driven motion
  • HTML Canvas for custom rendering
  • Performance optimization for smooth animations at 60fps
  • When to use each animation technique

Why It Matters

Well-designed animations improve perceived performance, guide user attention, and increase engagement. Studies show animated interfaces have 40% higher user satisfaction. But poorly implemented animations cause jank, trigger motion sickness, and hurt accessibility. Understanding the right tool for each job — CSS for simple transitions, Web Animations API for JavaScript-driven sequences, Canvas for complex graphics — is essential.

Real-world use: The Durga Antivirus Pro scanner uses a pulsing animation during active scanning, a progress animation for threat detection, and Canvas-based visualizations for network traffic patterns. Doda Browser uses smooth page transitions and a spring-based refresh indicator.

    flowchart LR
  A[Simple UI Motion] --> B[CSS Transitions]
  C[Keyframe Sequences] --> D[CSS Animations]
  E[JS-Driven Animation] --> F[Web Animations API]
  G[Complex Graphics] --> H[Canvas/WebGL]
  I[Performance] --> J[requestAnimationFrame]
  style B fill:#4af,color:#fff
  style D fill:#f90,color:#fff
  style F fill:#4a4,color:#fff
  style H fill:#f44,color:#fff
  

CSS Transitions

CSS transitions smoothly change property values over time.

/* Basic transition */
.button {
  background: #6366f1;
  color: white;
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.3s ease, transform 0.2s ease;
}

.button:hover {
  background: #4f46e5;
  transform: translateY(-2px);
}

.button:active {
  transform: translateY(0);
}

/* Multiple properties with different timings */
.card {
  opacity: 1;
  transform: scale(1) translateY(0);
  transition: 
    opacity 0.3s ease,
    transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); /* Bounce easing */
}

.card:hover {
  opacity: 0.9;
  transform: scale(1.02) translateY(-4px);
}

/* Enter animation with delay */
.item {
  opacity: 0;
  transform: translateX(-20px);
  transition: all 0.3s ease;
}

.item:nth-child(1) { transition-delay: 0s; }
.item:nth-child(2) { transition-delay: 0.1s; }
.item:nth-child(3) { transition-delay: 0.2s; }
.item:nth-child(4) { transition-delay: 0.3s; }

.item.visible {
  opacity: 1;
  transform: translateX(0);
}

Transition Properties

/* Timing functions */
.fast { transition-timing-function: ease; }
.bounce { transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); }
.elastic { transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); }

/* What to animate */
.transform-only { transition: transform 0.3s ease; }
/* Never animate: width, height, top, left, margin, padding — they trigger layout */

CSS Keyframe Animations

For multi-step sequences and repeating animations.

/* Pulse animation for scanning status */
@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7);
    transform: scale(1);
  }
  50% {
    transform: scale(1.05);
  }
  100% {
    box-shadow: 0 0 0 20px rgba(99, 102, 241, 0);
    transform: scale(1);
  }
}

.scan-indicator {
  width: 20px;
  height: 20px;
  background: #6366f1;
  border-radius: 50%;
  animation: pulse 1.5s ease-in-out infinite;
}

/* Loading spinner */
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #e0e0e0;
  border-top-color: #6366f1;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

/* Fade-in and slide-up for page content */
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.hero-content {
  animation: fadeInUp 0.6s ease-out both;
}

.hero-content .subtitle {
  animation: fadeInUp 0.6s ease-out 0.2s both;
}

.hero-content .cta {
  animation: fadeInUp 0.6s ease-out 0.4s both;
}

/* Animation control with JavaScript */
.paused {
  animation-play-state: paused;
}

/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Web Animations API

The Web Animations API lets you control animations from JavaScript with more power than CSS alone.

// Basic usage
const element = document.querySelector('.box');

const animation = element.animate([
  { transform: 'translateX(0)', opacity: 1 },
  { transform: 'translateX(300px)', opacity: 0.5 }
], {
  duration: 1000,
  easing: 'ease-in-out',
  fill: 'forwards'
});

// Promise-based completion
animation.finished.then(() => {
  console.log('Animation complete');
  element.style.transform = 'translateX(300px)';
});

// Playback control
animation.pause();
animation.play();
animation.reverse();
animation.cancel();
animation.finish();

// Spring physics animation
function springAnimation(element) {
  return element.animate([
    { transform: 'scale(1)' },
    { transform: 'scale(1.2)' },
    { transform: 'scale(0.9)' },
    { transform: 'scale(1.05)' },
    { transform: 'scale(1)' }
  ], {
    duration: 600,
    easing: 'ease-out',
    iterations: 1
  });
}

// Chained animations
async function animateSequence(elements) {
  for (const [index, el] of elements.entries()) {
    await el.animate([
      { opacity: '0', transform: 'translateY(20px)' },
      { opacity: '1', transform: 'translateY(0)' }
    ], {
      duration: 400,
      delay: index * 100, // Stagger
      fill: 'backwards'
    }).finished;
  }
}

// Keyframe effect with multiple properties
const keyframes = new KeyframeEffect(
  element,
  [
    { clipPath: 'circle(0%)', opacity: 0 },
    { clipPath: 'circle(100%)', opacity: 1 }
  ],
  { duration: 800, easing: 'ease-out', fill: 'forwards' }
);

const anim = new Animation(keyframes, document.timeline);
anim.play();

Scroll-Driven Animations (New in 2025+)

// Intersection Observer for scroll-triggered animations
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.style.animationPlayState = 'running';
      observer.unobserve(entry.target);
    }
  });
}, { threshold: 0.1 });

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  el.style.animationPlayState = 'paused';
  observer.observe(el);
});

Canvas Animations

Canvas provides pixel-level control for complex graphics, games, data visualizations, and particle effects.

// Basic Canvas setup
const canvas = document.getElementById('visualization');
const ctx = canvas.getContext('2d');

// Resize canvas
function resize() {
  canvas.width = canvas.clientWidth * devicePixelRatio;
  canvas.height = canvas.clientHeight * devicePixelRatio;
  ctx.scale(devicePixelRatio, devicePixelRatio);
}
window.addEventListener('resize', resize);
resize();

// Particle system for threat visualization
class Particle {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.vx = (Math.random() - 0.5) * 2;
    this.vy = (Math.random() - 0.5) * 2;
    this.size = Math.random() * 3 + 1;
    this.life = 1;
    this.color = `hsl(${Math.random() * 60 + 200}, 70%, 50%)`;
  }
  
  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.life -= 0.005;
  }
  
  draw(ctx) {
    ctx.globalAlpha = this.life;
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
    ctx.fill();
  }
}

class ParticleSystem {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.particles = [];
    this.running = false;
  }
  
  emit(x, y, count = 5) {
    for (let i = 0; i < count; i++) {
      this.particles.push(new Particle(x, y));
    }
  }
  
  update() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    
    this.particles.forEach(p => {
      p.update();
      p.draw(this.ctx);
    });
    
    // Remove dead particles
    this.particles = this.particles.filter(p => p.life > 0);
    
    if (this.running) {
      requestAnimationFrame(() => this.update());
    }
  }
  
  start() {
    this.running = true;
    this.update();
  }
  
  stop() {
    this.running = false;
  }
}

// Usage
const system = new ParticleSystem(document.getElementById('threat-canvas'));
system.start();

// Emit particles on threat detection
document.getElementById('scan-btn').addEventListener('click', () => {
  system.emit(200, 150, 20);
});

Performance Optimization

Animations that drop below 60fps cause jank. Here’s how to keep them smooth.

The Performance Checklist

/* ✅ Only animate these properties */
transform: translateX(100px);
transform: scale(1.5);
transform: rotate(45deg);
opacity: 0.5;

/* ❌ Never animate these (triggers layout) */
width: 200px;
height: 200px;
top: 50px;
left: 100px;
margin: 20px;
padding: 10px;
border-width: 2px;
// ✅ Use requestAnimationFrame instead of setInterval
function smoothLoop() {
  updateAnimation();
  requestAnimationFrame(smoothLoop);
}
requestAnimationFrame(smoothLoop);

// ❌ Don't use setInterval for animations
setInterval(updateAnimation, 16); // Doesn't sync with vsync

// ✅ Use will-change for GPU acceleration
const element = document.querySelector('.animated');
element.style.willChange = 'transform, opacity';

// Remove after animation (to free GPU memory)
animation.onfinish = () => {
  element.style.willChange = 'auto';
};

The FLIP Technique

FLIP (First, Last, Invert, Play) animates layout changes efficiently.

function flipAnimation(element, callback) {
  // First: record current position
  const first = element.getBoundingClientRect();
  
  // Perform the layout change
  callback();
  
  // Last: record new position
  const last = element.getBoundingClientRect();
  
  // Invert: calculate the difference
  const dx = first.left - last.left;
  const dy = first.top - last.top;
  const dw = first.width / last.width;
  const dh = first.height / last.height;
  
  // Play: animate back to current position
  element.animate([
    { transform: `translate(${dx}px, ${dy}px) scale(${1/dw}, ${1/dh})` },
    { transform: 'translate(0, 0) scale(1, 1)' }
  ], {
    duration: 300,
    easing: 'ease-out'
  });
}

// Usage
flipAnimation(card, () => {
  card.classList.add('expanded');
});

Common Errors

  1. Animating layout propertieswidth, height, top, left, margin force the browser to recalculate layout, causing jank. Animate transform and opacity only.
  2. No prefers-reduced-motion support — Many users have vestibular disorders. Always respect prefers-reduced-motion: reduce by stopping or replacing animations.
  3. CSS animations unattached to DOM — Animating display: none to display: block doesn’t work. Use opacity + visibility instead.
  4. Too many simultaneous animations — Each animation thread costs performance. Limit concurrent animations, especially on mobile.
  5. Not cleaning up requestAnimationFrame — Failing to cancel requestAnimationFrame on unmount causes memory leaks. Store the handle and call cancelAnimationFrame().

Practice Questions

  1. What properties should you animate for best performance? Only transform and opacity. These are handled by the compositor thread without triggering layout or paint.

  2. How does the Web Animations API differ from CSS animations? WAAPI provides JavaScript control over animation lifecycle (play, pause, reverse, finish), promise-based completion, and dynamic keyframe generation. CSS animations are declarative and cannot be paused or reversed programmatically without extra work.

  3. What is requestAnimationFrame and why is it better than setInterval? requestAnimationFrame tells the browser to run your code before the next repaint, syncing with the display’s refresh rate. setInterval runs regardless of paint cycles, causing missed frames or wasted work.

  4. What is prefers-reduced-motion and how do you support it? A CSS media query that detects user preference for reduced motion. Use it to disable or simplify animations, replacing motion with static transitions.

  5. When should you use Canvas instead of CSS/WAAPI for animations? Use Canvas for particle systems, data visualizations with thousands of elements, games, video processing, and pixel manipulation. Use CSS/WAAPI for UI animations on DOM elements.

Challenge

Build a dashboard widget that displays real-time network traffic using Canvas-based particle visualization. The particles should flow from left to right, change color based on traffic type (green = safe, yellow = suspicious, red = threat), and include smooth 60fps performance.

Real-World Task

Create an animated threat detection sequence for Durga Antivirus Pro. When a scan completes, show:

  1. A CSS pulse animation on the scan button
  2. A staggered fade-in of scan results using the Web Animations API
  3. A Canvas-based threat heatmap showing detected files
  4. A smooth counter animation showing threat count incrementing All animations must respect prefers-reduced-motion and maintain 60fps.

Previous: CSS Fundamentals | Related: HTML Canvas | Previous: JavaScript Fundamentals

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro