Animation transforms static interfaces into engaging, responsive experiences. JavaScript gives you full programmatic control over motion — something CSS alone cannot always provide. With JS you can react to user input in real time, build physics-based transitions, drive canvas or SVG scenes, and orchestrate complex sequenced animations that respond to data.
CSS animations and transitions are ideal for simple state changes — hover effects, loading spinners, accordion toggles. They run on the compositor thread and can leverage GPU acceleration with minimal JS. But when you need frame-by-frame control, dynamic values tied to scroll position, physics simulations, or sequenced multi-element choreography, JavaScript is the tool of choice.
The performance golden rule: animate only transform and opacity whenever possible. These two properties are handled by the GPU compositor and never trigger layout or paint. Animating top, left, width, or height forces the browser to recalculate layout every frame — a recipe for jank. The target is 60 frames per second: each frame has a budget of roughly 16.67 milliseconds. Exceed that and the user perceives stutter.
Approach
Best For
Performance
Control
CSS Transitions
Simple state changes, hover/focus effects
Compositor-thread, GPU-accelerated
Limited — start/end only
CSS @keyframes
Looping decorative animations, loading states
Compositor-thread, GPU-accelerated
Medium — keyframe sequences
Web Animations API
Programmatic CSS-like animations with JS control
Compositor-thread, optimized
High — play/pause/reverse
requestAnimationFrame
Custom animation loops, scroll-driven, physics
Main-thread, frame-synced
Full — every pixel, every frame
Canvas / WebGL
Particle systems, games, data visualization
GPU-accelerated bitmap
Full — pixel-level control
requestAnimationFrame (rAF)
requestAnimationFrame is the foundation of JavaScript animation. It tells the browser you want to perform an animation and requests that the browser call your callback function before the next repaint. The callback receives a high-resolution timestamp, enabling smooth, frame-synchronized motion.
Why not setInterval? Because setInterval fires at fixed intervals regardless of whether the browser is ready to paint. It can fire twice between frames or skip frames entirely. It also continues running in background tabs, wasting CPU. requestAnimationFrame is synchronized with the display refresh rate, pauses in background tabs, and receives a precise timestamp for delta-time calculations.
raf-basic.js
JavaScript
1
// Basic rAF syntax
2
const id = requestAnimationFrame((timestamp) => {
3
console.log('Next frame at:', timestamp);
4
});
5
6
// Cancel if needed before it fires
7
cancelAnimationFrame(id);
8
9
// Chaining — call rAF inside the callback for continuous animation
// 4. Batched with layout/paint — no wasted frames
preview
Building an Animation Loop
A robust animation system separates concerns: state management, physics/easing calculations, and rendering. The key to frame-rate-independent animation is delta time — the elapsed time between frames. Instead of incrementing position by a fixed amount each frame, multiply speed by the time elapsed. This ensures animations run at the same visual speed on a 30fps device and a 144fps display.
Easing functions map elapsed time (0 to 1) to a progress curve, creating natural-feeling motion. Linear motion looks robotic. Real objects accelerate, decelerate, and overshoot. Easing is the difference between a UI that feels alive and one that feels mechanical.
easings.js
JavaScript
1
// Common easing functions — all take t (0→1) and return eased value (0→1)
2
const easings = {
3
linear: (t) => t,
4
easeInQuad: (t) => t * t,
5
easeOutQuad: (t) => t * (2 - t),
6
easeInOutQuad:(t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
7
8
easeInCubic: (t) => t * t * t,
9
easeOutCubic: (t) => (--t) * t * t + 1,
10
easeInOutCubic:(t) => t < 0.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
// Animating with easing — interpolate from start to end
32
function animateValue(element, prop, start, end, duration, easing) {
33
const startTime = performance.now();
34
function tick(now) {
35
const elapsed = now - startTime;
36
const t = Math.min(elapsed / duration, 1);
37
const eased = easing(t);
38
const value = start + (end - start) * eased;
39
element.style[prop] = value + 'px';
40
if (t < 1) requestAnimationFrame(tick);
41
}
42
requestAnimationFrame(tick);
43
}
preview
Element.animate() — Web Animations API
The Web Animations API bridges CSS animations and JavaScript control. element.animate() creates an Animation object that you can play, pause, reverse, cancel, and seek — giving you the power of CSS keyframes with the flexibility of programmatic control. It runs off the main thread in most browsers, making it both powerful and performant.
web-animations-api.js
JavaScript
1
// Basic element.animate() — keyframes and options
Scroll-triggered animations reveal elements as the user scrolls down the page, creating a sense of progression and discovery. The IntersectionObserver API is the performant way to detect when elements enter the viewport — it avoids expensive scroll event listeners and runs off the main thread.
scroll-reveal.js
JavaScript
1
// IntersectionObserver — scroll reveal
2
const observer = new IntersectionObserver(
3
(entries) => {
4
entries.forEach((entry) => {
5
if (entry.isIntersecting) {
6
entry.target.classList.add('revealed');
7
observer.unobserve(entry.target); // animate once
8
}
9
});
10
},
11
{
12
threshold: 0.15, // trigger when 15% visible
13
rootMargin: '0px 0px -50px 0px' // trigger 50px before entering
Parallax creates a depth illusion by moving background layers at different speeds as the user scrolls. The simplest CSS approach is background-attachment: fixed, but it lacks control and doesn't work reliably on mobile. JavaScript parallax reads the scroll position and applies a translateY transform, which is GPU-accelerated and works consistently across devices.
parallax.js
JavaScript
1
// JS parallax — translate based on scroll position
Drag-and-drop interaction requires tracking pointer position across multiple events: mousedown/touchstart to initiate, mousemove/touchmove to update position, and mouseup/touchend to release. The pattern calculates the delta between the initial click and current pointer, then applies a transform.
Touch devices require gesture detection — swipe, pinch-to-zoom, rotation — beyond simple drag. Swipe detection compares the start and end touch positions, measuring both direction and velocity. Pinch-to-zoom tracks the distance between two touch points over time. These raw touch events are the building blocks for all touch interactions.
swipe-detector.js
JavaScript
1
// Swipe detection — direction + velocity
2
class SwipeDetector {
3
constructor(element, options = {}) {
4
this.element = element;
5
this.threshold = options.threshold || 50; // min px distance
6
this.velocity = options.velocity || 0.3; // min px/ms
7
this.onSwipe = options.onSwipe || (() => {});
8
9
this.startX = 0;
10
this.startY = 0;
11
this.startTime = 0;
12
13
element.addEventListener('touchstart', (e) => {
14
const touch = e.touches[0];
15
this.startX = touch.clientX;
16
this.startY = touch.clientY;
17
this.startTime = Date.now();
18
}, { passive: true });
19
20
element.addEventListener('touchend', (e) => {
21
const touch = e.changedTouches[0];
22
const dx = touch.clientX - this.startX;
23
const dy = touch.clientY - this.startY;
24
const dt = Date.now() - this.startTime;
25
26
const absDx = Math.abs(dx);
27
const absDy = Math.abs(dy);
28
29
if (Math.max(absDx, absDy) < this.threshold) return;
const dy = touches[0].clientY - touches[1].clientY;
35
return Math.sqrt(dx * dx + dy * dy);
36
}
37
}
Physics-Based Animation
Spring animations feel natural because they model real-world physics — mass, stiffness, and damping. Unlike easing functions that follow a fixed curve, springs react to velocity and displacement, producing organic motion that overshoots, bounces, and settles naturally. This is the technique behind iOS rubber-banding, Material Design motion, and physics-based UI libraries.
spring.js
JavaScript
1
// Spring simulation — position, velocity, stiffness, damping
2
class Spring {
3
constructor(options = {}) {
4
this.position = options.from || 0;
5
this.target = options.to || 1;
6
this.velocity = 0;
7
this.stiffness = options.stiffness || 180; // spring constant
The HTML5 Canvas API provides a pixel-level drawing surface. Unlike DOM animation, canvas redraws the entire scene each frame — which makes it ideal for particle systems, games, and data visualizations where you're updating hundreds or thousands of elements. The 2D context offers shapes, gradients, paths, and image manipulation. For heavier work, OffscreenCanvas moves rendering to a Web Worker.
SVG paths can be animated by manipulating stroke-dasharray and stroke-dashoffset. When the dash offset equals the total path length, the stroke is invisible. Animating it to zero draws the path. This creates the popular "line drawing" effect. JS can drive this with precise timing, chaining multiple paths, or responding to scroll position.
const observer = new IntersectionObserver((entries) => {
26
entries.forEach((entry) => {
27
if (entry.isIntersecting) {
28
drawPath();
29
observer.unobserve(entry.target);
30
}
31
});
32
}, { threshold: 0.3 });
33
34
observer.observe(path);
preview
Performance Optimization
Smooth animation at 60fps requires avoiding layout recalculation, minimizing paint, and keeping the main thread free. The browser rendering pipeline is: JavaScript → Style → Layout → Paint → Composite. Animating transform and opacity skips Layout and Paint entirely — the GPU handles it during Composite.
Property
Triggers Layout
Triggers Paint
GPU-Accelerated
transform
No
No
Yes
opacity
No
No
Yes
top / left
Yes
Yes
No
width / height
Yes
Yes
No
margin / padding
Yes
Yes
No
box-shadow
No
Yes
Sometimes
perf-optimize.js
JavaScript
1
// Layout thrashing — reading forces pending writes to flush
2
// BAD
3
elements.forEach(el => {
4
const height = el.offsetHeight; // forces layout
5
el.style.height = (height * 2) + 'px'; // write
6
});
7
8
// GOOD — batch reads, then batch writes
9
const heights = elements.map(el => el.offsetHeight); // read all
10
elements.forEach((el, i) => {
11
el.style.height = (heights[i] * 2) + 'px'; // write all
12
});
13
14
// will-change — hint to browser to promote to compositor layer
// Now draw from a Web Worker thread — main thread stays free
✓
best practice
Use the Chrome DevTools Performance panel to record animation frames. Look for the green "Recalculate Style" and red "Layout" bars in the main thread. If you see them during your animation, you're triggering expensive layout/paint. Aim for the main thread to only show green "Paint" and "Composite" bars.
Reduced Motion
Some users experience motion sickness, vestibular disorders, or have their OS set to minimize animations. The prefers-reduced-motion media query and its JavaScript equivalent let you respect this preference. Animations should be replaced with instant transitions, static indicators, or very subtle motion.
reduced-motion.js
JavaScript
1
// CSS — respect reduced motion preference
2
// @media (prefers-reduced-motion: reduce) {
3
// *, *::before, *::after {
4
// animation-duration: 0.01ms !important;
5
// animation-iteration-count: 1 !important;
6
// transition-duration: 0.01ms !important;
7
// }
8
// }
9
10
// JavaScript — check reduced motion preference
11
const prefersReducedMotion = window.matchMedia(
12
'(prefers-reduced-motion: reduce)'
13
);
14
15
function shouldAnimate() {
16
return !prefersReducedMotion.matches;
17
}
18
19
// Usage — conditionally run animations
20
if (shouldAnimate()) {
21
element.animate([
22
{ transform: 'translateY(20px)', opacity: 0 },
23
{ transform: 'translateY(0)', opacity: 1 },
24
], { duration: 600 });
25
} else {
26
// Instant reveal — no motion
27
element.style.opacity = '1';
28
}
29
30
// Listen for changes (user toggles setting while page is open)
Always respect prefers-reduced-motion. Ignoring it can cause nausea, dizziness, and anxiety in users with vestibular disorders. Replace motion with opacity fades, static highlights, or instant state changes. Never use !importantto override the user's system preference.
Common Mistakes
common-mistakes.js
JavaScript
1
// MISTAKE 1: Using setInterval for animation
2
// BAD — drifts, wastes frames, runs in background tabs
3
setInterval(() => {
4
element.style.left = x + 'px';
5
x += 2;
6
}, 16);
7
8
// FIX — use requestAnimationFrame
9
let x = 0;
10
function animate() {
11
x += 2;
12
element.style.transform = `translateX(${x}px)`;
13
requestAnimationFrame(animate);
14
}
15
requestAnimationFrame(animate);
16
17
18
// MISTAKE 2: Animating layout properties
19
// BAD — triggers layout recalculation every frame