SVG Animation: 20 Live Examples With Code

SVG animation moves or redraws vector shapes with CSS, SMIL or JavaScript. CSS keyframes handle transforms, opacity and the stroke-dasharray plus stroke-dashoffset line-drawing trick. SMIL tags like <animate> can morph paths and follow motion paths without scripts. Libraries such as GSAP add timelines and scroll control. Lottie suits designer-made After Effects animations.

Which technique for which effect
EffectSimplest technique
Draw a line or iconCSS stroke-dashoffset
Spin, pulse, bounceCSS transform keyframes
Morph one shape into anotherSMIL <animate attributeName="d">
Move along a curveSMIL <animateMotion> or CSS offset-path
Sequenced, scroll-linked scenesGSAP or another JS library

Updated

Every animation on this page is running live from the code next to it, in a frame with scripts switched off, so what you copy is exactly what works without JavaScript. Each one also stops for visitors who ask their system for reduced motion. A line-drawing builder at the top writes the stroke trick for any path you paste.

20 live SVG animations

All twenty run without JavaScript and include a reduced-motion rule. Edit any of them in place.

Line drawing: checkmark

The classic stroke trick with pathLength=1, so the dash values do not depend on the real length.

<style>
.draw { stroke-dasharray: 1; stroke-dashoffset: 1; animation: draw 1.2s ease-out forwards infinite alternate; }
@keyframes draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 60 60" width="90"><circle cx="30" cy="30" r="26" fill="none" stroke="#16a34a" stroke-width="4" pathLength="1" class="draw"/>
<path d="M18 31l8 8 16-17" fill="none" stroke="#16a34a" stroke-width="4" stroke-linecap="round" pathLength="1" class="draw"/></svg>

Line drawing: handwriting stroke

A longer path drawn slowly with a linear ease reads like a pen moving.

<style>
path { stroke-dasharray: 1; stroke-dashoffset: 1; animation: pen 3s linear infinite; }
@keyframes pen { 70%, 100% { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 220 60" width="260"><path d="M5 40c20-30 30-30 30 0s15-35 30-5 10 15 25-10 20 10 35 5 15-25 30-5 20 15 55-5" fill="none" stroke="#1d4ed8" stroke-width="3" stroke-linecap="round" pathLength="1"/></svg>

Loader: spinning arc

A circle with a partial dash rotates; transform-box keeps the spin centred.

<style>
.arc { transform-box: fill-box; transform-origin: center; animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 50 50" width="60" role="img" aria-label="Loading"><circle cx="25" cy="25" r="20" fill="none" stroke="#e5e7eb" stroke-width="5"/>
<circle class="arc" cx="25" cy="25" r="20" fill="none" stroke="#4F98FB" stroke-width="5" stroke-linecap="round" pathLength="100" stroke-dasharray="30 70"/></svg>

Loader: three dots

Staggered animation-delay values turn one keyframe set into a wave.

<style>
circle { animation: up 0.9s ease-in-out infinite; }
circle:nth-child(2) { animation-delay: .15s; } circle:nth-child(3) { animation-delay: .3s; }
@keyframes up { 50% { transform: translateY(-8px); opacity: .4; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 60 30" width="90"><circle cx="10" cy="18" r="5" fill="#334155"/><circle cx="30" cy="18" r="5" fill="#334155"/><circle cx="50" cy="18" r="5" fill="#334155"/></svg>

Pulse ring

Scale and fade together for a radar or live indicator.

<style>
.ring { transform-box: fill-box; transform-origin: center; animation: ping 1.6s ease-out infinite; }
@keyframes ping { from { transform: scale(.4); opacity: 1; } to { transform: scale(1.6); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 60 60" width="80"><circle class="ring" cx="30" cy="30" r="14" fill="none" stroke="#dc2626" stroke-width="3"/><circle cx="30" cy="30" r="7" fill="#dc2626"/></svg>

Equaliser bars

Each bar scales on the Y axis from its base with its own duration.

<style>
rect { transform-box: fill-box; transform-origin: bottom; animation: eq 1s ease-in-out infinite alternate; }
rect:nth-child(2) { animation-duration: .7s; } rect:nth-child(3) { animation-duration: 1.3s; } rect:nth-child(4) { animation-duration: .9s; }
@keyframes eq { from { transform: scaleY(.2); } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 50 40" width="80"><rect x="4" y="5" width="7" height="35" fill="#7c3aed"/><rect x="16" y="5" width="7" height="35" fill="#7c3aed"/><rect x="28" y="5" width="7" height="35" fill="#7c3aed"/><rect x="40" y="5" width="7" height="35" fill="#7c3aed"/></svg>

Chart bars growing

Bars grow once on load. Using forwards keeps the final state, and the reduced-motion rule simply shows the finished chart.

<style>
.bar { transform-box: fill-box; transform-origin: bottom; transform: scaleY(0); animation: grow .8s cubic-bezier(.2,.8,.2,1) forwards; }
.bar:nth-of-type(2) { animation-delay: .1s; } .bar:nth-of-type(3) { animation-delay: .2s; } .bar:nth-of-type(4) { animation-delay: .3s; }
@keyframes grow { to { transform: scaleY(1); } }
@media (prefers-reduced-motion: reduce) { .bar { animation: none; transform: none; } }
</style>
<svg viewBox="0 0 120 80" width="220" role="img" aria-label="Orders by quarter: 20, 35, 50, 70">
<line x1="0" y1="79" x2="120" y2="79" stroke="#cbd5e1"/>
<rect class="bar" x="8" y="59" width="20" height="20" fill="#0ea5e9"/><rect class="bar" x="36" y="44" width="20" height="35" fill="#0ea5e9"/>
<rect class="bar" x="64" y="29" width="20" height="50" fill="#0ea5e9"/><rect class="bar" x="92" y="9" width="20" height="70" fill="#0ea5e9"/></svg>

Donut progress

The same dash idea on a circle. Rotate it -90 degrees so it starts at the top.

<style>
.val { stroke-dasharray: 100; stroke-dashoffset: 100; animation: fill 1.5s ease-out forwards; transform: rotate(-90deg); transform-origin: 50% 50%; }
@keyframes fill { to { stroke-dashoffset: 28; } }
@media (prefers-reduced-motion: reduce) { .val { animation: none; stroke-dashoffset: 28; } }
</style>
<svg viewBox="0 0 42 42" width="100" role="img" aria-label="72 percent"><circle cx="21" cy="21" r="15.9" fill="none" stroke="#e2e8f0" stroke-width="5"/>
<circle class="val" cx="21" cy="21" r="15.9" fill="none" stroke="#f59e0b" stroke-width="5" pathLength="100"/>
<text x="21" y="24" text-anchor="middle" font-size="8" font-family="system-ui">72%</text></svg>

Icon transition: menu to close

Hover or focus the button. Each line moves and rotates with a transition, no keyframes needed.

<style>
button { border: 0; background: none; padding: 8px; cursor: pointer; }
line { stroke: #111; stroke-width: 3; stroke-linecap: round; transition: transform .3s, opacity .2s; transform-box: fill-box; transform-origin: center; }
button:hover .t, button:focus-visible .t { transform: translateY(8px) rotate(45deg); }
button:hover .m, button:focus-visible .m { opacity: 0; }
button:hover .b, button:focus-visible .b { transform: translateY(-8px) rotate(-45deg); }
@media (prefers-reduced-motion: reduce) { line { transition: none; } }
</style>
<button aria-label="Menu"><svg viewBox="0 0 30 24" width="40" aria-hidden="true"><line class="t" x1="3" y1="4" x2="27" y2="4"/><line class="m" x1="3" y1="12" x2="27" y2="12"/><line class="b" x1="3" y1="20" x2="27" y2="20"/></svg></button>

Heartbeat

Two quick scale steps then a rest, all in one keyframe list.

<style>
path { transform-box: fill-box; transform-origin: center; animation: beat 1.2s ease-in-out infinite; }
@keyframes beat { 0%, 40%, 100% { transform: scale(1); } 15% { transform: scale(1.18); } 28% { transform: scale(1.08); } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 32 30" width="70"><path d="M16 28S2 19 2 10a7 7 0 0 1 14-2 7 7 0 0 1 14 2c0 9-14 18-14 18z" fill="#e11d48"/></svg>

Morph with the CSS d property

Works in Chromium and Firefox. Where d is not supported the shape stays a square. Both paths use the same commands in the same order.

<style>
path { d: path("M10 10 L50 10 L50 50 L10 50 Z"); animation: morph 2s ease-in-out infinite alternate; }
@keyframes morph { to { d: path("M30 5 L55 30 L30 55 L5 30 Z"); } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 60 60" width="90"><path fill="#14b8a6"/></svg>

Morph with SMIL animate

SMIL changes the d attribute itself, which has wider support than the CSS d property. The still copy shows for reduced motion.

<style>.still{display:none} @media (prefers-reduced-motion: reduce){ .moving{display:none} .still{display:inline} }</style>
<svg class="moving" viewBox="0 0 60 60" width="90"><path fill="#6366f1" d="M10 10 L50 10 L50 50 L10 50 Z">
  <animate attributeName="d" dur="2s" repeatCount="indefinite" values="M10 10 L50 10 L50 50 L10 50 Z; M30 5 L55 30 L30 55 L5 30 Z; M10 10 L50 10 L50 50 L10 50 Z"/>
</path></svg>
<svg class="still" viewBox="0 0 60 60" width="90"><path fill="#6366f1" d="M10 10 L50 10 L50 50 L10 50 Z"/></svg>

Follow a path with animateMotion

rotate=auto turns the shape to face the direction of travel.

<style>.still{display:none} @media (prefers-reduced-motion: reduce){ .moving{display:none} .still{display:inline} }</style>
<svg class="moving" viewBox="0 0 200 80" width="240"><path id="track" d="M10 60 C60 0 140 0 190 60" fill="none" stroke="#cbd5e1" stroke-dasharray="4 4"/>
<polygon points="-6,-5 8,0 -6,5" fill="#ea580c"><animateMotion dur="3s" repeatCount="indefinite" rotate="auto"><mpath href="#track"/></animateMotion></polygon></svg>
<svg class="still" viewBox="0 0 200 80" width="240"><path d="M10 60 C60 0 140 0 190 60" fill="none" stroke="#cbd5e1" stroke-dasharray="4 4"/></svg>

Rotating gear with animateTransform

The rotate values take the centre point as their second and third numbers.

<style>.still{display:none} @media (prefers-reduced-motion: reduce){ .moving{display:none} .still{display:inline} }</style>
<svg class="moving" viewBox="0 0 40 40" width="80"><g><path d="M20 6l3 4 5-1 1 5 4 3-3 4 1 5-5 1-3 4-3-4-5 1-1-5-4-3 3-4-1-5 5-1z" fill="#475569"/><circle cx="20" cy="20" r="5" fill="#fff"/>
<animateTransform attributeName="transform" type="rotate" from="0 20 20" to="360 20 20" dur="4s" repeatCount="indefinite"/></g></svg>
<svg class="still" viewBox="0 0 40 40" width="80"><path d="M20 6l3 4 5-1 1 5 4 3-3 4 1 5-5 1-3 4-3-4-5 1-1-5-4-3 3-4-1-5 5-1z" fill="#475569"/><circle cx="20" cy="20" r="5" fill="#fff"/></svg>

Shifting gradient

SMIL animates the gradient stop colours, something CSS keyframes on stop-color can also do.

<style>.still{display:none} @media (prefers-reduced-motion: reduce){ .moving{display:none} .still{display:inline} }</style>
<svg class="moving" viewBox="0 0 200 40" width="240"><defs><linearGradient id="g"><stop offset="0" stop-color="#4F98FB"><animate attributeName="stop-color" values="#4F98FB;#a855f7;#4F98FB" dur="4s" repeatCount="indefinite"/></stop>
<stop offset="1" stop-color="#22d3ee"><animate attributeName="stop-color" values="#22d3ee;#f97316;#22d3ee" dur="4s" repeatCount="indefinite"/></stop></linearGradient></defs><rect width="200" height="40" rx="8" fill="url(#g)"/></svg>
<svg class="still" viewBox="0 0 200 40" width="240"><rect width="200" height="40" rx="8" fill="#4F98FB"/></svg>

Endless wave

A path twice as wide as the view slides left by exactly one period, so the loop has no seam.

<style>
.w { animation: slide 3s linear infinite; }
@keyframes slide { to { transform: translateX(-100px); } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 100 40" width="220"><path class="w" d="M0 20 q25 -15 50 0 t50 0 t50 0 t50 0 V40 H0Z" fill="#38bdf8"/></svg>

Notification bell shake on hover

Rotation from the top of the bell, triggered by hover or keyboard focus.

<style>
a { display: inline-block; padding: 6px; }
.bell { transform-box: fill-box; transform-origin: top center; }
a:hover .bell, a:focus-visible .bell { animation: ring .6s ease-in-out; }
@keyframes ring { 20% { transform: rotate(15deg); } 40% { transform: rotate(-12deg); } 60% { transform: rotate(8deg); } 80% { transform: rotate(-4deg); } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<a href="#" aria-label="Notifications"><svg viewBox="0 0 24 24" width="44" aria-hidden="true"><path class="bell" d="M12 3a6 6 0 0 0-6 6v4l-2 3h16l-2-3V9a6 6 0 0 0-6-6zm-2 15a2 2 0 0 0 4 0" fill="none" stroke="#111" stroke-width="2"/></svg></a>

Marching ants selection

A dashed stroke with an animated offset, the look of a selection box in image editors.

<style>
rect { stroke-dasharray: 6 4; animation: march .6s linear infinite; }
@keyframes march { to { stroke-dashoffset: -10; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 120 70" width="200"><rect x="5" y="5" width="110" height="60" fill="none" stroke="#111" stroke-width="1.5"/></svg>

Data flowing along a connector

Short dashes on a curved line move continuously to suggest direction.

<style>
.flow { stroke-dasharray: 2 8; animation: run 1s linear infinite; }
@keyframes run { to { stroke-dashoffset: -10; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 200 70" width="240"><circle cx="15" cy="35" r="10" fill="#0f766e"/><circle cx="185" cy="35" r="10" fill="#0f766e"/>
<path d="M25 35 C80 -5 120 75 175 35" fill="none" stroke="#99f6e4" stroke-width="4"/><path class="flow" d="M25 35 C80 -5 120 75 175 35" fill="none" stroke="#0f766e" stroke-width="4" stroke-linecap="round"/></svg>

Bouncing ball with squash

Squash on impact sells the weight. transform-origin at the bottom keeps the ball on the floor while it squashes.

<style>
.ball { transform-box: fill-box; transform-origin: bottom; animation: bounce 1s cubic-bezier(.5,0,.5,1) infinite; }
@keyframes bounce { 0%, 100% { transform: translateY(-50px); } 45% { transform: translateY(0) scale(1); } 50% { transform: translateY(0) scale(1.25, .75); } 55% { transform: translateY(0) scale(1); } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
</style>
<svg viewBox="0 0 60 90" width="70"><line x1="0" y1="85" x2="60" y2="85" stroke="#cbd5e1"/><circle class="ball" cx="30" cy="75" r="10" fill="#f43f5e"/></svg>

CSS vs SMIL vs JavaScript (GSAP)

Three ways to animate SVG
CSSSMILJavaScript library such as GSAP
Needs a scriptNoNoYes
Works inside an img tagOnly CSS written inside the SVG fileYesNo
Path morphingCSS d property, not in Safari at the time of writingYes, same command structureYes, plugins can morph unlike shapes
Motion along a pathoffset-pathanimateMotionYes
Timelines and sequencingDelays onlybegin values can chain eventsFull timelines, labels, reversing
Scroll-linkedScroll-driven animations where supportedNoYes, for example ScrollTrigger
Reduced motionMedia queryHide or pause with scriptCheck matchMedia in code

The same line drawing and bar chart in GSAP

Code only, since the demo frames have scripts off. GSAP is free to use under its own licence, which is not MIT, so read its terms before shipping.

import { gsap } from "gsap";

const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

if (!reduce) {
  const tl = gsap.timeline({ defaults: { ease: "power2.out" } });
  tl.fromTo(".check", { strokeDashoffset: 1 }, { strokeDashoffset: 0, duration: 1 })
    .from(".bar", { scaleY: 0, transformOrigin: "50% 100%", stagger: 0.1, duration: 0.6 }, "-=0.4");
}

Performance

A handful of animated icons costs almost nothing. Trouble starts with hundreds of animated nodes, large filters such as blur on moving shapes, or animating attributes that force the whole drawing to repaint. Test on a mid-range phone, not only a laptop.

Pause what nobody can see. Loaders should be removed when content arrives, and decorative loops below the fold can start only when they scroll into view.

SVG or Lottie

Lottie stores an animation as JSON exported from After Effects and plays it with a JavaScript player, so the designer's timing, easing and layers survive exactly. The price is the player script plus a JSON file that can grow large for detailed scenes.

Plain SVG plus CSS needs no player and is easy to restyle with your brand colours or to hook up to hover and focus. If you can sketch the motion as a few keyframes, write it in CSS; if it needs a motion designer's timeline, Lottie is the better handoff.

How it works

  1. Paste an SVG path into the line-drawing builder, set the speed and easing, and copy the generated code.
  2. Browse the twenty live demos. Edit the code in any of them and the result updates.
  3. Use the comparison table to choose between CSS, SMIL and a JavaScript library.
  4. Read the Lottie section before exporting animations from a design tool.

Tips

Frequently asked questions

How does the SVG line drawing effect work?

Give the stroke a dash as long as the path with stroke-dasharray, push it out of view with an equal stroke-dashoffset, then animate the offset to 0. The dash slides into place and the line appears to draw itself. pathLength="1" lets you use 1 for both values.

Is SMIL deprecated?

No. Chrome announced a plan to deprecate it in 2015 and then suspended that plan. SMIL elements such as animate, animateTransform and animateMotion work in all current major browsers and are part of the SVG specification.

Can CSS morph SVG paths?

Partly. The CSS d property accepts path() values and can be animated in Chromium browsers and Firefox, but Safari has not supported it at the time of writing, so check caniuse. Both paths must have the same number and type of commands. SMIL animate on the d attribute works more widely.

Should I use Lottie or SVG?

Use plain SVG with CSS for icons, loaders and simple illustrations you can describe in code. Use Lottie when a motion designer builds a complex character or scene in After Effects and you want to play it back faithfully. Lottie needs a player library; plain SVG does not.

How do I respect reduced motion in SVG animations?

Wrap CSS animations in a prefers-reduced-motion media query or turn them off inside it. CSS cannot pause SMIL, so show a still version of the graphic in that media query, or call pauseAnimations() on the svg element from a script.

Related: SVG optimizer, CSS animation generator, Scroll animations, CSS transitions, CSS transforms.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap