How to Animate SVG: CSS, SMIL, WAAPI, and GSAP Compared

·12 min read

SVG is the only image format on the web you can animate without touching JavaScript and without a single pixel ever being decoded. That makes it the best primitive on the platform for everything from a spinner in a button to a full interactive data visualisation. The flip side: there are at least four legitimate ways to do it, the internet still argues about which is best, and most articles conflate “animating an SVG” with “animating a single attribute on a single element.” This guide separates the four approaches, shows when each one wins, and ends with a working live demo built using the simplest of them.

The live demo first

Everything below this paragraph is a single inline SVG with three rotating rings and a self-drawing curve. There is no JavaScript on this page. The animation is driven entirely by CSS @keyframes and respects your prefers-reduced-motion setting — if you have reduced motion enabled at the OS level, the rings stop and the path renders fully drawn.

Live CSS-animated SVG demoThree concentric rings rotate at different speeds while a curved path draws and undraws itself. All animation is driven by pure CSS keyframes and respects the prefers-reduced-motion media query.

The four approaches, briefly

Animating SVG on the web in 2026 means picking one of these four tools, sometimes combined:

  1. CSS animations and transitions. The default, simplest, fastest. Works on any property the CSS engine knows about.
  2. SMIL. SVG’s built-in declarative animation language. Lives inside the SVG file itself. Slightly out of fashion but still supported and still useful in narrow cases.
  3. Web Animations API. The native browser JavaScript API for animating any DOM property. No dependency, no CSS file, full programmatic control.
  4. JavaScript libraries. Mostly GSAP. Where you go when you need timeline orchestration, path morphing, scroll-tied sequences, or cross-browser bug fixes.

A separate, distinct family — Lottie — is also worth knowing about, but it is not really “animating SVG.” We’ll get to why later.

Four ways to animate SVG, comparedA grid comparing CSS animations, SMIL, the Web Animations API, and GSAP across four dimensions: best use case, performance, JavaScript bundle weight, and learning curve. CSS, SMIL, and WAAPI add no JavaScript; GSAP adds about thirty kilobytes. CSS is GPU-accelerated; SMIL is decent; WAAPI is native; GSAP is excellent. CSS is trivial to learn, SMIL has quirky XML syntax, WAAPI is moderate, and GSAP is the easiest of the libraries.ApproachBest forPerfJS weightLearnCSSLoops, hover, simple iconsGPU-accelerated0 KBTrivialSMILSelf-contained animated SVG filesDecent0 KBQuirky XMLWAAPIProgrammatic, no libraryNative0 KBModerateGSAPSequences, morph, scroll-tiedExcellent~30 KBEasy

1. CSS animations: the default

When CSS works for what you need, do not reach for anything else. CSS animations on SVG are GPU-accelerated for transforms and opacity, require no JavaScript, are inert when a user has reduced-motion enabled (assuming you wire it up — see below), and have the lowest possible bundle cost: zero bytes.

The mental model is exactly the same as animating any other DOM element. SVG elements accept CSS, so:

<svg viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="20" class="pulse" />
</svg>

<style>
  @keyframes pulse {
    50% { transform: scale(1.4); opacity: 0.6; }
  }
  .pulse {
    transform-origin: center;
    transform-box: fill-box;
    animation: pulse 1.6s ease-in-out infinite;
  }
</style>

Two non-obvious gotchas: the transform-origin on SVG elements defaults to 0 0, not 50% 50% as on HTML elements, so you usually want to specify it explicitly. And transform-box: fill-box tells the browser to compute the origin relative to the element’s own bounding box rather than the SVG viewport — this is almost always what you want, and the difference between “the circle scales in place” and “the circle flies into the corner.”

What CSS cannot do: animate the d attribute of a path (no path morphing), and synchronise multiple animations into a timeline with anything resembling pleasant ergonomics. For both of those you reach for JavaScript.

The reduced-motion ritual

Always include this. Always. WCAG 2.2 requires it, your users with vestibular disorders need it, and the cost is one media query:

@media (prefers-reduced-motion: reduce) {
  .pulse, .spin, .draw { animation: none !important; }
}

2. SMIL: the misunderstood relic

SMIL (Synchronized Multimedia Integration Language) is SVG’s own declarative animation language, defined inside the SVG element itself. For most of the 2010s it was treated as deprecated — Chrome announced an intent to remove it in 2015, then walked it back when enough authors complained — and it has been hovering in “not recommended but not going away” status ever since.

The pitch is genuinely compelling: a SMIL-animated SVG is a single self-contained file. You can drop it into <img>, email it to someone, or save it to disk, and it animates on its own. No CSS, no JavaScript, no embed context required.

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <circle r="10" fill="#111">
    <animateMotion dur="3s" repeatCount="indefinite"
                   path="M 0 50 Q 50 0 100 50 Q 50 100 0 50 Z" />
  </circle>
</svg>

That circle now follows the path forever, with no other code involved. No CSS-only or WAAPI approach can match that for portability.

The reasons SMIL fell out of favour are real: the syntax is verbose XML, the timing model is its own beast, and Internet Explorer never supported it (which mattered until ~2017 and now does not). For new web work where you control the embedding context, CSS or WAAPI are almost always cleaner. For an animated icon you want to ship to emails, slide decks, or social-media uploads where it’ll be loaded as <img>, SMIL is still the right tool.

3. Web Animations API: native programmatic control

The Web Animations API (WAAPI) is the browser’s native JavaScript animation interface. Every modern browser ships it. No library, no polyfill, no CSS file:

const circle = document.querySelector('circle');
const anim = circle.animate(
  [
    { transform: 'scale(1)', opacity: 1 },
    { transform: 'scale(1.4)', opacity: 0.6, offset: 0.5 },
    { transform: 'scale(1)', opacity: 1 },
  ],
  { duration: 1600, iterations: Infinity, easing: 'ease-in-out' }
);

// Pause, reverse, set playback rate, listen for finish events
anim.pause();
anim.playbackRate = 2;
anim.finished.then(() => console.log('done'));

WAAPI hits the sweet spot for “I want CSS-style animation performance, but I need to start it from JavaScript.” It is GPU-accelerated where CSS is, integrates cleanly with React effects and Vue lifecycle hooks, and gives you the playback controls (pause, reverse, scrub, listen) that CSS alone cannot.

Where it falls short: complex sequences. WAAPI has no built-in timeline that lets you say “animate A, then B, then C and D together, then start E halfway through D.” You can build that out of finished promises and delay juggling, but at that point you’re writing a worse version of GSAP’s timeline, and you should probably just use GSAP.

4. GSAP and friends: when you need real animation

The GreenSock Animation Platform (GSAP) is the unchallenged industry-standard JavaScript animation library, used everywhere from one-page marketing sites to Formula 1 telemetry dashboards. As of 2024 it became fully free for commercial use, including its previously paid plugins. For SVG specifically, GSAP gives you several things nothing else does:

  • The timeline. A first-class API for sequencing animations across multiple elements with labels, offsets, and relative positioning. Once you’ve used it you cannot unsee its absence in WAAPI.
  • MorphSVG. Smoothly morphs one SVG path into another, handling point counts, command differences, and direction mismatches automatically. Doing this by hand is genuinely hard.
  • DrawSVG. Animates the drawing of any path with a single line — no stroke-dasharray calculation needed (you can also do this in CSS, see below).
  • MotionPath. Moves an element along an SVG path, with the option to align rotation to the path’s tangent.
  • Cross-browser bug fixes. Several long-standing transform-origin and matrix-interpolation bugs in Safari and Firefox are silently worked around. This is a real value-add for production code.
import gsap from 'gsap';
import { MotionPathPlugin } from 'gsap/MotionPathPlugin';
gsap.registerPlugin(MotionPathPlugin);

gsap.to('#dot', {
  duration: 3,
  repeat: -1,
  motionPath: {
    path: '#track',
    align: '#track',
    autoRotate: true,
    alignOrigin: [0.5, 0.5],
  },
});

The main cost of GSAP is bundle size: about 30 KB minified+gzipped for the core, plus more for each plugin you use. For an animation-heavy site that price is trivially worth it. For a single loading spinner it is not.

Lighter-weight alternatives exist. Anime.js (~7 KB) covers most of the common cases and is genuinely good. Motion One (~3.8 KB) is the modern WAAPI-based option from the Framer team, with a delightful API. Both are good answers when you want library ergonomics without GSAP’s footprint and you do not need MorphSVG.

The path-drawing trick (no library needed)

One of the most-loved SVG animations is “the path draws itself in” — a logo unspooling, a chart line tracing forward. You can do it in pure CSS without any library:

<path id="logo" d="M..." pathLength="100" fill="none"
      stroke="#111" stroke-width="2" />

<style>
  #logo {
    stroke-dasharray: 100;
    stroke-dashoffset: 100;
    animation: draw 2s ease-out forwards;
  }
  @keyframes draw { to { stroke-dashoffset: 0; } }
</style>

The trick: stroke-dasharray defines the dash pattern, stroke-dashoffset shifts where it starts. Set them equal and the path is invisible. Animate offset to zero and the dash slides through, drawing the path. The pathLength="100" attribute lets you treat any path as if it were 100 units long, so you don’t have to measure the actual length in JavaScript. (We use this trick in the demo at the top of this article.)

Lottie: the different beast

Lottie is an animation format and runtime built by Airbnb, originally to bridge After Effects animations into mobile apps. The pipeline is: designer animates in After Effects, exports via the Bodymovin plugin to a JSON file, the Lottie runtime renders it on the page.

The thing to understand about Lottie: it is not really “an SVG animation.” The runtime can render to SVG, Canvas, or HTML backends, and the JSON file is a serialised After Effects timeline, not an SVG. You use Lottie when you have an animator who lives in After Effects and you want to ship their work pixel-perfect. You do not use Lottie because you want to animate an SVG you already have.

The runtime adds substantial weight (~150 KB for lottie-web, ~30 KB for the lighter dotLottie player), the JSON files are often larger than equivalent hand-coded SVG animations, and you cannot easily author or edit them without After Effects. For high-fidelity designer-driven motion, Lottie is excellent. For everything else, one of the four approaches above is lighter and more flexible.

What to actually use, by use case

  • Loading spinner: CSS. animation: spin 1s linear infinite; is a one-liner.
  • Hover-state icon micro-interaction: CSS transitions. Even simpler than animations.
  • Logo that draws itself in on page load: CSS with the stroke-dashoffset trick.
  • An SVG file that needs to animate on its own when emailed or saved: SMIL.
  • A chart that should animate when it scrolls into view: WAAPI inside an IntersectionObserver callback, or GSAP’s ScrollTrigger plugin.
  • A multi-stage onboarding animation: GSAP timeline. Anything else will be twice the code and worse.
  • Path morphing — turning a hamburger icon into an X, a shape transforming into another shape: GSAP MorphSVG. Doing this by hand is a research project.
  • A complex character animation from an After Effects file: Lottie.

Performance: what to animate, what to avoid

The same rules that apply to web animation in general apply to SVG. Animating transform and opacity is cheap because the browser can offload them to the GPU. Animatingcx, cy, x, y, width, height, or r is more expensive because each frame triggers a layout in the SVG coordinate system.

For most icon-scale work this difference is invisible. For a chart with a hundred animated elements it matters a lot, and you should prefer transform-based animation wherever possible — wrap an element in a <g> and translate the group rather than animating the inner x/y directly.

One related note: SVG animations live in the same compositing layer as the rest of the document. A massive complex animated SVG can cause repaints in the surrounding HTML. will-change: transform on the SVG element promotes it to its own compositing layer and fixes this — but use it sparingly, since each promoted layer has a small constant cost.

The honest summary

If you only remember one rule: start with CSS, escalate to WAAPI when you need programmatic control, escalate to GSAP when you need timelines or morphing, and use SMIL for the specific case of self-contained animated SVG files. Lottie is its own world; reach for it only when an After Effects animation is the input.

And do not skip prefers-reduced-motion. The whole demo at the top of this page respects it. Yours should too.


Related on svg.dog: our guide to inline vs. external SVG covers the embed-context trade-offs that determine which animation approaches actually work, and our SVGO guide shows how to keep SVGO from stripping your animations during optimisation.

Optimize SVGs instantly — no setup needed

500 free optimizations per month. No credit card required.

Get a free API key →