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.
| Effect | Simplest technique |
|---|---|
| Draw a line or icon | CSS stroke-dashoffset |
| Spin, pulse, bounce | CSS transform keyframes |
| Morph one shape into another | SMIL <animate attributeName="d"> |
| Move along a curve | SMIL <animateMotion> or CSS offset-path |
| Sequenced, scroll-linked scenes | GSAP 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.
All twenty run without JavaScript and include a reduced-motion rule. Edit any of them in place.
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>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>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>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>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>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>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>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>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>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>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>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>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>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>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>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>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>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>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>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 | SMIL | JavaScript library such as GSAP | |
|---|---|---|---|
| Needs a script | No | No | Yes |
| Works inside an img tag | Only CSS written inside the SVG file | Yes | No |
| Path morphing | CSS d property, not in Safari at the time of writing | Yes, same command structure | Yes, plugins can morph unlike shapes |
| Motion along a path | offset-path | animateMotion | Yes |
| Timelines and sequencing | Delays only | begin values can chain events | Full timelines, labels, reversing |
| Scroll-linked | Scroll-driven animations where supported | No | Yes, for example ScrollTrigger |
| Reduced motion | Media query | Hide or pause with script | Check matchMedia in code |
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");
}
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.
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.
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.
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.
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.
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.
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.
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.