SVG Animation: How to Create Stunning Animated Graphics With Code
SVG — Scalable Vector Graphics — is the web's best-kept animation secret. While most creative coders reach for Canvas or WebGL, SVG offers something neither can: infinitely sharp graphics at any resolution, built-in animation primitives, and a DOM you can manipulate with the same JavaScript you already know. Every line, curve, and shape is a living object you can transform, morph, and choreograph.
The math behind SVG animation is pure geometry: Bézier curves defined by control points, transformation matrices for rotation and scaling, path interpolation for morphing one shape into another. Canvas gives you pixels. WebGL gives you shaders. SVG gives you shapes — and shapes remember what they are. You can animate a circle into a star and back, because both are just different sets of numbers describing the same path structure.
This guide covers eight working SVG animation systems you can build in your browser. Every example uses vanilla JavaScript — no GSAP, no Snap.svg, no frameworks. Just the SVG specification, a bit of trigonometry, and the requestAnimationFrame loop that drives the modern web.
Why SVG for animation?
Canvas draws pixels and immediately forgets them. To animate, you clear and redraw everything every frame. SVG is different: each element persists in the DOM as a node with attributes you can change. Move a circle? Set its cx attribute. Scale a group? Apply a transform. The browser handles the rendering, anti-aliasing, and hit-testing for you.
Key advantages of SVG animation:
- Resolution independent — looks crisp on any screen, any zoom level
- Accessible — screen readers can describe SVG content; Canvas is opaque
- Styleable — CSS transitions and animations work on SVG attributes
- Interactive — every element can have event listeners (click, hover, drag)
- Small file size — a complex SVG animation can be under 5KB; the same in Canvas might need 50KB of code
The tradeoff: SVG slows down with thousands of elements (the DOM isn't free). For 10,000 particles, use Canvas. For 200 beautifully animated shapes with crisp edges and hover states, SVG is unbeatable.
Example 1: Stroke drawing — the line reveals itself
The most iconic SVG animation: a path that appears to draw itself. This technique uses stroke-dasharray and stroke-dashoffset to reveal a stroke progressively.
const NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 400 400');
svg.style.width = '400px';
svg.style.height = '400px';
svg.style.background = '#0a0a0a';
document.body.appendChild(svg);
// Create a complex path — a recursive tree
function treePath(x, y, angle, len, depth) {
if (depth === 0 || len < 2) return '';
var x2 = x + Math.cos(angle) * len;
var y2 = y + Math.sin(angle) * len;
var path = 'M' + x.toFixed(1) + ',' + y.toFixed(1) +
'L' + x2.toFixed(1) + ',' + y2.toFixed(1);
var spread = 0.4 + depth * 0.05;
path += treePath(x2, y2, angle - spread, len * 0.72, depth - 1);
path += treePath(x2, y2, angle + spread, len * 0.72, depth - 1);
return path;
}
var d = treePath(200, 380, -Math.PI / 2, 80, 9);
var path = document.createElementNS(NS, 'path');
path.setAttribute('d', d);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', 'url(#grad)');
path.setAttribute('stroke-width', '1.2');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
// Gradient for the tree
var defs = document.createElementNS(NS, 'defs');
var grad = document.createElementNS(NS, 'linearGradient');
grad.id = 'grad';
grad.setAttribute('x1', '0');
grad.setAttribute('y1', '1');
grad.setAttribute('x2', '0');
grad.setAttribute('y2', '0');
var s1 = document.createElementNS(NS, 'stop');
s1.setAttribute('offset', '0%');
s1.setAttribute('stop-color', '#4a2');
var s2 = document.createElementNS(NS, 'stop');
s2.setAttribute('offset', '100%');
s2.setAttribute('stop-color', '#e8f');
grad.appendChild(s1);
grad.appendChild(s2);
defs.appendChild(grad);
svg.insertBefore(defs, svg.firstChild);
// Animate the stroke
var totalLen = path.getTotalLength();
path.style.strokeDasharray = totalLen;
path.style.strokeDashoffset = totalLen;
var start = null;
var duration = 5000;
function animate(ts) {
if (!start) start = ts;
var progress = Math.min((ts - start) / duration, 1);
var ease = 1 - Math.pow(1 - progress, 3);
path.style.strokeDashoffset = totalLen * (1 - ease);
if (progress < 1) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
A tree grows from nothing, each branch unfurling from trunk to tip. The magic is getTotalLength() — SVG knows exactly how long any path is, so you can reveal it at any speed. This technique works for handwriting, circuit boards, maps, constellations — anything expressible as a path. Lumitree uses a similar approach when new branches sprout on the tree.
Example 2: Path morphing — one shape becomes another
SVG paths can smoothly morph between shapes if they share the same number and type of commands. This example morphs between a circle, a star, a square, and a flower.
var NS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 400 400');
svg.style.width = '400px';
svg.style.height = '400px';
svg.style.background = '#0a0a0a';
document.body.appendChild(svg);
var cx = 200, cy = 200, pts = 64;
function shapePoints(type) {
var points = [];
for (var i = 0; i < pts; i++) {
var angle = (i / pts) * Math.PI * 2 - Math.PI / 2;
var r;
if (type === 'circle') {
r = 120;
} else if (type === 'star') {
r = i % 2 === 0 ? 140 : 60;
} else if (type === 'square') {
var a = angle + Math.PI / 4;
r = 120 / Math.max(Math.abs(Math.cos(a)), Math.abs(Math.sin(a)));
} else {
r = 100 + 40 * Math.cos(angle * 5);
}
points.push({
x: cx + r * Math.cos(angle),
y: cy + r * Math.sin(angle)
});
}
return points;
}
function pointsToPath(points) {
return 'M' + points.map(function(p) {
return p.x.toFixed(1) + ',' + p.y.toFixed(1);
}).join('L') + 'Z';
}
function lerp(a, b, t) { return a + (b - a) * t; }
function lerpPoints(from, to, t) {
return from.map(function(p, i) {
return { x: lerp(p.x, to[i].x, t), y: lerp(p.y, to[i].y, t) };
});
}
var shapes = ['circle', 'star', 'square', 'flower'];
var colors = ['#4af', '#f4a', '#af4', '#fa4'];
var shapeIndex = 0;
var morphProgress = 0;
var path = document.createElementNS(NS, 'path');
path.setAttribute('fill', 'none');
path.setAttribute('stroke', colors[0]);
path.setAttribute('stroke-width', '2');
svg.appendChild(path);
var label = document.createElementNS(NS, 'text');
label.setAttribute('x', '200');
label.setAttribute('y', '370');
label.setAttribute('text-anchor', 'middle');
label.setAttribute('fill', '#666');
label.setAttribute('font-size', '13');
label.textContent = 'click to morph';
svg.appendChild(label);
var fromPts = shapePoints(shapes[0]);
var toPts = shapePoints(shapes[1]);
var animating = false;
function drawFrame() {
var current = lerpPoints(fromPts, toPts, morphProgress);
path.setAttribute('d', pointsToPath(current));
var c1 = colors[shapeIndex % colors.length];
var c2 = colors[(shapeIndex + 1) % colors.length];
path.setAttribute('stroke', morphProgress < 0.5 ? c1 : c2);
}
function animateMorph() {
morphProgress += 0.015;
var ease = morphProgress < 0.5
? 2 * morphProgress * morphProgress
: 1 - Math.pow(-2 * morphProgress + 2, 2) / 2;
var current = lerpPoints(fromPts, toPts, ease);
path.setAttribute('d', pointsToPath(current));
var ci = shapeIndex % colors.length;
var ni = (shapeIndex + 1) % colors.length;
path.setAttribute('stroke', ease < 0.5 ? colors[ci] : colors[ni]);
if (morphProgress < 1) {
requestAnimationFrame(animateMorph);
} else {
morphProgress = 0;
shapeIndex = (shapeIndex + 1) % shapes.length;
fromPts = shapePoints(shapes[shapeIndex]);
toPts = shapePoints(shapes[(shapeIndex + 1) % shapes.length]);
animating = false;
}
}
drawFrame();
svg.addEventListener('click', function() {
if (!animating) { animating = true; animateMorph(); }
});
Click the shape and it smoothly transforms: circle to star, star to square, square to flower, then back. The key insight is that morphing requires equal-length point arrays — we sample every shape at the same 64 angles, so each point has a clear counterpart to interpolate toward. Easing (the quadratic in/out function) makes the motion feel organic rather than robotic.
Example 3: Motion along a path — objects that follow curves
SVG has a built-in superpower: getPointAtLength(). Given any path, you can get the exact x,y coordinates at any distance along it. This lets objects follow arbitrarily complex curves with zero math.
var NS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 500 400');
svg.style.width = '500px';
svg.style.height = '400px';
svg.style.background = '#0a0a0a';
document.body.appendChild(svg);
// An infinity-loop path
var track = document.createElementNS(NS, 'path');
var d = 'M100,200 C100,50 250,50 250,200 C250,350 400,350 400,200 C400,50 250,50 250,200 C250,350 100,350 100,200Z';
track.setAttribute('d', d);
track.setAttribute('fill', 'none');
track.setAttribute('stroke', '#222');
track.setAttribute('stroke-width', '1');
svg.appendChild(track);
var totalLen = track.getTotalLength();
var count = 12;
var dots = [];
for (var i = 0; i < count; i++) {
var g = document.createElementNS(NS, 'g');
var circle = document.createElementNS(NS, 'circle');
circle.setAttribute('r', String(4 + (i % 3) * 2));
circle.setAttribute('fill', 'none');
circle.setAttribute('stroke-width', '1.5');
var hue = (i / count) * 360;
circle.setAttribute('stroke', 'hsl(' + hue + ', 80%, 65%)');
g.appendChild(circle);
var trail = document.createElementNS(NS, 'path');
trail.setAttribute('fill', 'none');
trail.setAttribute('stroke', 'hsl(' + hue + ', 80%, 65%)');
trail.setAttribute('stroke-width', '0.8');
trail.setAttribute('opacity', '0.3');
svg.appendChild(trail);
svg.appendChild(g);
dots.push({ g: g, circle: circle, trail: trail, offset: (i / count) * totalLen, history: [] });
}
var speed = 1.5;
function animate() {
dots.forEach(function(dot) {
dot.offset = (dot.offset + speed) % totalLen;
var pt = track.getPointAtLength(dot.offset);
dot.g.setAttribute('transform', 'translate(' + pt.x + ',' + pt.y + ')');
dot.history.push({ x: pt.x, y: pt.y });
if (dot.history.length > 60) dot.history.shift();
if (dot.history.length > 1) {
var td = 'M' + dot.history[0].x.toFixed(1) + ',' + dot.history[0].y.toFixed(1);
for (var j = 1; j < dot.history.length; j++) {
td += 'L' + dot.history[j].x.toFixed(1) + ',' + dot.history[j].y.toFixed(1);
}
dot.trail.setAttribute('d', td);
}
});
requestAnimationFrame(animate);
}
animate();
Twelve colored orbs chase each other along an infinity loop, each trailing a fading history behind it. The path can be anything — a hand-drawn curve, text converted to outlines, a map route. This technique powers animated infographics, loading spinners, and mathematical curve visualizations across the web.
Example 4: Staggered animations — the cascade effect
Create a grid of SVG elements that animate in sequence, producing a wave-like cascade. Staggering is what separates amateur animation from professional motion design.
var NS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 400 400');
svg.style.width = '400px';
svg.style.height = '400px';
svg.style.background = '#0a0a0a';
document.body.appendChild(svg);
var cols = 15, rows = 15;
var cellW = 400 / cols, cellH = 400 / rows;
var rects = [];
for (var row = 0; row < rows; row++) {
for (var col = 0; col < cols; col++) {
var rect = document.createElementNS(NS, 'rect');
var x = col * cellW + cellW * 0.15;
var y = row * cellH + cellH * 0.15;
var w = cellW * 0.7;
var h = cellH * 0.7;
rect.setAttribute('x', String(x));
rect.setAttribute('y', String(y));
rect.setAttribute('width', String(w));
rect.setAttribute('height', String(h));
rect.setAttribute('rx', '3');
rect.setAttribute('fill', 'none');
rect.setAttribute('stroke-width', '1');
var dist = Math.sqrt(Math.pow(col - cols / 2, 2) + Math.pow(row - rows / 2, 2));
rects.push({ el: rect, col: col, row: row, dist: dist });
svg.appendChild(rect);
}
}
var t = 0;
function animate() {
t += 0.03;
rects.forEach(function(r) {
var wave = Math.sin(t - r.dist * 0.3);
var scale = 0.5 + wave * 0.5;
var hue = (r.dist * 25 + t * 30) % 360;
var lightness = 40 + wave * 25;
var alpha = 0.4 + wave * 0.6;
var cx = r.col * cellW + cellW / 2;
var cy = r.row * cellH + cellH / 2;
r.el.setAttribute('transform',
'translate(' + cx + ',' + cy + ') ' +
'scale(' + scale.toFixed(3) + ') ' +
'rotate(' + (wave * 45).toFixed(1) + ') ' +
'translate(' + (-cx) + ',' + (-cy) + ')');
r.el.setAttribute('stroke', 'hsl(' + hue.toFixed(0) + ', 75%, ' + lightness.toFixed(0) + '%)');
r.el.setAttribute('opacity', alpha.toFixed(2));
});
requestAnimationFrame(animate);
}
animate();
A grid of squares pulses outward from the center like a ripple in a pond. Each element scales, rotates, and shifts color based on its distance from the center plus a time offset — the stagger. This single principle (delay = f(distance)) creates the illusion of a coordinated, living system. Change r.dist * 0.3 to r.col * 0.2 for a left-to-right wave, or (r.col + r.row) * 0.15 for a diagonal sweep.
Example 5: Generative SVG patterns — algorithmic tiles
SVG's <pattern> element tiles a small graphic infinitely. Combine it with generative drawing and you get infinite, resolution-independent wallpapers from a few lines of code.
var NS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 400 400');
svg.style.width = '400px';
svg.style.height = '400px';
document.body.appendChild(svg);
var defs = document.createElementNS(NS, 'defs');
svg.appendChild(defs);
var tileSize = 40;
var pattern = document.createElementNS(NS, 'pattern');
pattern.id = 'genPat';
pattern.setAttribute('width', String(tileSize));
pattern.setAttribute('height', String(tileSize));
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
defs.appendChild(pattern);
function generateTile() {
while (pattern.firstChild) pattern.removeChild(pattern.firstChild);
var bg = document.createElementNS(NS, 'rect');
bg.setAttribute('width', String(tileSize));
bg.setAttribute('height', String(tileSize));
bg.setAttribute('fill', '#0a0a0a');
pattern.appendChild(bg);
var hue = Math.random() * 360;
var type = Math.floor(Math.random() * 4);
var half = tileSize / 2;
if (type === 0) {
// Diagonal lines
for (var i = -1; i <= 2; i++) {
var line = document.createElementNS(NS, 'line');
line.setAttribute('x1', String(i * tileSize));
line.setAttribute('y1', '0');
line.setAttribute('x2', String(i * tileSize - tileSize));
line.setAttribute('y2', String(tileSize));
line.setAttribute('stroke', 'hsl(' + hue + ', 70%, 55%)');
line.setAttribute('stroke-width', '1');
line.setAttribute('opacity', '0.6');
pattern.appendChild(line);
}
} else if (type === 1) {
// Quarter circles from corners
var corners = [[0, 0], [tileSize, 0], [tileSize, tileSize], [0, tileSize]];
corners.forEach(function(c, j) {
var arc = document.createElementNS(NS, 'circle');
arc.setAttribute('cx', String(c[0]));
arc.setAttribute('cy', String(c[1]));
arc.setAttribute('r', String(half * 0.9));
arc.setAttribute('fill', 'none');
arc.setAttribute('stroke', 'hsl(' + ((hue + j * 30) % 360) + ', 65%, 50%)');
arc.setAttribute('stroke-width', '0.8');
arc.setAttribute('opacity', '0.5');
pattern.appendChild(arc);
});
} else if (type === 2) {
// Concentric diamonds
for (var k = 3; k > 0; k--) {
var s = (k / 3) * half;
var diamond = document.createElementNS(NS, 'polygon');
diamond.setAttribute('points',
half + ',' + (half - s) + ' ' +
(half + s) + ',' + half + ' ' +
half + ',' + (half + s) + ' ' +
(half - s) + ',' + half);
diamond.setAttribute('fill', 'none');
diamond.setAttribute('stroke', 'hsl(' + ((hue + k * 40) % 360) + ', 70%, ' + (40 + k * 10) + '%)');
diamond.setAttribute('stroke-width', '1');
pattern.appendChild(diamond);
}
} else {
// Cross-hatch dots
for (var dx = 0; dx < 3; dx++) {
for (var dy = 0; dy < 3; dy++) {
var dot = document.createElementNS(NS, 'circle');
var px = (dx + 0.5) * (tileSize / 3);
var py = (dy + 0.5) * (tileSize / 3);
dot.setAttribute('cx', String(px));
dot.setAttribute('cy', String(py));
dot.setAttribute('r', String(1.5 + Math.random() * 2));
dot.setAttribute('fill', 'hsl(' + ((hue + (dx + dy) * 20) % 360) + ', 60%, 55%)');
dot.setAttribute('opacity', String(0.4 + Math.random() * 0.4));
pattern.appendChild(dot);
}
}
}
}
generateTile();
var rect = document.createElementNS(NS, 'rect');
rect.setAttribute('width', '400');
rect.setAttribute('height', '400');
rect.setAttribute('fill', 'url(#genPat)');
svg.appendChild(rect);
var label = document.createElementNS(NS, 'text');
label.setAttribute('x', '200');
label.setAttribute('y', '390');
label.setAttribute('text-anchor', 'middle');
label.setAttribute('fill', '#555');
label.setAttribute('font-size', '12');
label.textContent = 'click for a new pattern';
svg.appendChild(label);
svg.addEventListener('click', function() { generateTile(); });
Click and a new infinite pattern fills the canvas — diagonal hatching, quarter-circle arcs, nested diamonds, or scattered dots. The browser tiles the pattern automatically, handling all the repetition. This is how wallpaper generators, textile designs, and geometric art tools work under the hood. The tile is tiny; the result is infinite.
Example 6: Animated SVG filters — liquid distortion
SVG filters are the platform's hidden superweapon. Turbulence, displacement, blur, and compositing — all GPU-accelerated, all animatable. This example creates a living, breathing liquid distortion effect.
var NS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 400 400');
svg.style.width = '400px';
svg.style.height = '400px';
svg.style.background = '#0a0a0a';
document.body.appendChild(svg);
var defs = document.createElementNS(NS, 'defs');
svg.appendChild(defs);
var filter = document.createElementNS(NS, 'filter');
filter.id = 'liquid';
filter.setAttribute('x', '-20%');
filter.setAttribute('y', '-20%');
filter.setAttribute('width', '140%');
filter.setAttribute('height', '140%');
var turb = document.createElementNS(NS, 'feTurbulence');
turb.setAttribute('type', 'fractalNoise');
turb.setAttribute('baseFrequency', '0.015');
turb.setAttribute('numOctaves', '3');
turb.setAttribute('seed', '5');
turb.setAttribute('result', 'noise');
filter.appendChild(turb);
var disp = document.createElementNS(NS, 'feDisplacementMap');
disp.setAttribute('in', 'SourceGraphic');
disp.setAttribute('in2', 'noise');
disp.setAttribute('scale', '25');
disp.setAttribute('xChannelSelector', 'R');
disp.setAttribute('yChannelSelector', 'G');
filter.appendChild(disp);
defs.appendChild(filter);
// Create geometric shapes to distort
var g = document.createElementNS(NS, 'g');
g.setAttribute('filter', 'url(#liquid)');
svg.appendChild(g);
var rings = 8;
for (var i = 0; i < rings; i++) {
var circle = document.createElementNS(NS, 'circle');
circle.setAttribute('cx', '200');
circle.setAttribute('cy', '200');
circle.setAttribute('r', String(40 + i * 22));
circle.setAttribute('fill', 'none');
var hue = (i / rings) * 280;
circle.setAttribute('stroke', 'hsl(' + hue + ', 75%, 60%)');
circle.setAttribute('stroke-width', '2');
g.appendChild(circle);
}
// Center shape
var hex = document.createElementNS(NS, 'polygon');
var hexPts = [];
for (var j = 0; j < 6; j++) {
var a = (j / 6) * Math.PI * 2 - Math.PI / 2;
hexPts.push((200 + 30 * Math.cos(a)).toFixed(1) + ',' + (200 + 30 * Math.sin(a)).toFixed(1));
}
hex.setAttribute('points', hexPts.join(' '));
hex.setAttribute('fill', 'none');
hex.setAttribute('stroke', '#fff');
hex.setAttribute('stroke-width', '1.5');
g.appendChild(hex);
// Animate the turbulence
var seed = 0;
function animate() {
seed += 0.5;
turb.setAttribute('seed', String(Math.floor(seed) % 100));
var freq = 0.012 + Math.sin(seed * 0.02) * 0.005;
turb.setAttribute('baseFrequency', freq.toFixed(4));
var scale = 20 + Math.sin(seed * 0.03) * 12;
disp.setAttribute('scale', scale.toFixed(1));
requestAnimationFrame(animate);
}
animate();
Concentric rings and a central hexagon shimmer and warp as if submerged in water. The feTurbulence filter generates Perlin noise (the same algorithm from our Perlin noise guide), and feDisplacementMap uses it to push pixels around. By animating the seed and frequency, the distortion constantly evolves. This filter-based approach uses zero JavaScript geometry — the GPU does all the heavy lifting.
Example 7: SVG particle system — circles with physics
While Canvas is the traditional home for particle systems, SVG particles have a unique advantage: each one is a DOM element you can style, click, and inspect. Here's a gravity-driven system with 150 particles.
var NS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 400 400');
svg.style.width = '400px';
svg.style.height = '400px';
svg.style.background = '#0a0a0a';
document.body.appendChild(svg);
var particles = [];
var count = 150;
for (var i = 0; i < count; i++) {
var circle = document.createElementNS(NS, 'circle');
var r = 1 + Math.random() * 3;
circle.setAttribute('r', String(r));
var hue = Math.random() * 60 + 160;
circle.setAttribute('fill', 'hsl(' + hue + ', 70%, 60%)');
circle.setAttribute('opacity', '0.7');
svg.appendChild(circle);
particles.push({
el: circle,
x: Math.random() * 400,
y: Math.random() * 400,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
r: r,
mass: r * r
});
}
var mouse = { x: 200, y: 200 };
svg.addEventListener('mousemove', function(e) {
var rect = svg.getBoundingClientRect();
mouse.x = (e.clientX - rect.left) * (400 / rect.width);
mouse.y = (e.clientY - rect.top) * (400 / rect.height);
});
function animate() {
particles.forEach(function(p) {
var dx = mouse.x - p.x;
var dy = mouse.y - p.y;
var dist = Math.sqrt(dx * dx + dy * dy) + 1;
var force = Math.min(50 / (dist * dist) * p.mass, 0.5);
p.vx += (dx / dist) * force;
p.vy += (dy / dist) * force;
p.vx *= 0.98;
p.vy *= 0.98;
p.x += p.vx;
p.y += p.vy;
if (p.x < 0) { p.x = 0; p.vx *= -0.5; }
if (p.x > 400) { p.x = 400; p.vx *= -0.5; }
if (p.y < 0) { p.y = 0; p.vy *= -0.5; }
if (p.y > 400) { p.y = 400; p.vy *= -0.5; }
p.el.setAttribute('cx', p.x.toFixed(1));
p.el.setAttribute('cy', p.y.toFixed(1));
var speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
var alpha = 0.3 + Math.min(speed * 0.15, 0.7);
p.el.setAttribute('opacity', alpha.toFixed(2));
});
requestAnimationFrame(animate);
}
animate();
Hover your mouse and 150 particles swirl toward it like iron filings around a magnet. Each particle has mass proportional to its visual size — heavier particles resist the pull, lighter ones dart quickly. The DOM overhead limits us to hundreds rather than thousands of particles, but each one is crisp at any zoom level and could have its own tooltip, click handler, or CSS transition. For massive particle systems, use Canvas; for interactive, inspectable ones, SVG shines.
Example 8: Interactive kinetic art — the full piece
Let's combine everything into a single interactive kinetic sculpture: rotating arcs that respond to your mouse, with staggered timing, color cycling, and smooth elastic motion.
var NS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 400 400');
svg.style.width = '400px';
svg.style.height = '400px';
svg.style.background = '#0a0a0a';
document.body.appendChild(svg);
var cx = 200, cy = 200;
var numRings = 20;
var arcs = [];
for (var i = 0; i < numRings; i++) {
var arc = document.createElementNS(NS, 'path');
arc.setAttribute('fill', 'none');
arc.setAttribute('stroke-linecap', 'round');
arc.setAttribute('stroke-width', String(2 + (i % 3)));
svg.appendChild(arc);
arcs.push({
el: arc,
radius: 30 + i * 9,
angle: 0,
targetAngle: 0,
span: Math.PI * (0.3 + Math.random() * 0.8),
speed: 0.5 + Math.random() * 1.5,
direction: Math.random() > 0.5 ? 1 : -1
});
}
var mouse = { x: cx, y: cy };
svg.addEventListener('mousemove', function(e) {
var rect = svg.getBoundingClientRect();
mouse.x = (e.clientX - rect.left) * (400 / rect.width);
mouse.y = (e.clientY - rect.top) * (400 / rect.height);
});
function describeArc(cx, cy, r, startAngle, endAngle) {
var x1 = cx + r * Math.cos(startAngle);
var y1 = cy + r * Math.sin(startAngle);
var x2 = cx + r * Math.cos(endAngle);
var y2 = cy + r * Math.sin(endAngle);
var largeArc = (endAngle - startAngle) > Math.PI ? 1 : 0;
return 'M' + x1.toFixed(2) + ',' + y1.toFixed(2) +
' A' + r + ',' + r + ' 0 ' + largeArc + ' 1 ' +
x2.toFixed(2) + ',' + y2.toFixed(2);
}
var t = 0;
function animate() {
t += 0.01;
var mouseAngle = Math.atan2(mouse.y - cy, mouse.x - cx);
var mouseDist = Math.sqrt(Math.pow(mouse.x - cx, 2) + Math.pow(mouse.y - cy, 2));
var influence = Math.min(mouseDist / 200, 1);
arcs.forEach(function(a, i) {
var baseAngle = t * a.speed * a.direction + (i * Math.PI * 2 / numRings);
a.targetAngle = baseAngle + mouseAngle * influence * 0.3;
a.angle += (a.targetAngle - a.angle) * 0.08;
var startA = a.angle - a.span / 2;
var endA = a.angle + a.span / 2;
a.el.setAttribute('d', describeArc(cx, cy, a.radius, startA, endA));
var hue = (i * 15 + t * 40) % 360;
var light = 45 + Math.sin(t + i * 0.3) * 15;
a.el.setAttribute('stroke', 'hsl(' + hue.toFixed(0) + ', 75%, ' + light.toFixed(0) + '%)');
});
requestAnimationFrame(animate);
}
animate();
Twenty arcs orbit the center, each at its own speed and direction, their colors slowly cycling through the spectrum. Move your mouse and they lean toward it with elastic easing — a subtle gravitational pull that makes the whole sculpture feel alive. The arcs describe SVG arc paths (the A command), which are mathematically perfect circles at any scale.
This is kinetic art in its purest form: simple geometric elements, simple rules, complex emergent behavior. The sculptor Alexander Calder built physical mobiles from wire and sheet metal. We build digital ones from arcs and transforms.
SVG animation techniques reference
Here's a quick reference for the core SVG animation patterns:
- Attribute animation — set element attributes directly in a
requestAnimationFrameloop. Most flexible, works everywhere. - CSS animations — use
@keyframesandanimationon SVG elements for simple repeating motions (rotate, fade, pulse). No JavaScript needed. - CSS transitions — add
transition: all 0.3sto SVG elements, then change attributes on hover or click for smooth interpolation. - SMIL animation — SVG's native
<animate>element. Declarative and powerful, but deprecated in Chrome (later un-deprecated). Use with caution. - Web Animations API —
element.animate(keyframes, options)works on SVG elements. Combines CSS animation power with JavaScript control. - SVG filters —
feTurbulence,feDisplacementMap,feGaussianBlur,feColorMatrix. GPU-accelerated, powerful, but limited to what the filter primitives support.
Performance tips for SVG animation
SVG animation has specific performance characteristics you should understand:
- Element count matters — keep animated elements under 500. Each SVG node is a DOM object with memory and layout cost.
- Use
transformover geometry — moving a circle by changing itstransformattribute is faster than changingcxandcy, because transforms don't trigger layout recalculation. - Group static elements — put non-animated elements in a
<g>so the browser can cache them. - Prefer
opacityandtransform— these are the only properties most browsers can animate on the GPU compositor. Color and geometry changes go through the CPU. - Use
will-change: transform— hint to the browser that an element will animate, so it can promote it to its own compositor layer. - Throttle attribute updates — set attributes once per frame in
requestAnimationFrame, never in event handlers directly.
What to build next
These eight examples are starting points. Here are directions to explore:
- Build a drawing tool where each stroke is an SVG path you can edit, recolor, and animate after drawing
- Create an animated data visualization where bars grow, lines draw, and labels fade in with staggered timing
- Make a color palette generator where each swatch is an SVG element that morphs between harmonies
- Combine SVG patterns with animation for endlessly evolving geometric wallpapers
- Build a procedural SVG landscape that generates mountains, trees, and clouds as vector shapes
- Use
clipPathandmaskto create reveal animations — text that appears through an animated aperture
On Lumitree, some micro-worlds use SVG for their crisp, scalable visuals — especially the geometric art and visual poetry branches where text and shape coexist. SVG animation keeps these worlds sharp on any device, from a 4K monitor to a phone screen, all within the 50KB constraint. Because vector math is compact: a single SVG path command like A 100,100 0 1 1 200,200 describes a perfect arc that would take hundreds of pixels to render as a bitmap. Math is always smaller than pixels — and SVG is just math made visible.