How to Animate an SVG with CSS, SMIL and JavaScript
An SVG is a document, not a picture. Every shape in it is an element with attributes, which means the browser can animate it using the same machinery it uses for a div — no video, no player library, no exported frames. This is a walkthrough of the three ways to do that, the two traps that catch people first, and how to leave a still version for anyone who asked not to see motion.
1. Decide how the file reaches the page first
This is the step that is skipped, and it invalidates everything after it. An SVG written inline in your HTML is part of the document: your stylesheet and your scripts can reach every node inside it. The same file loaded through <img src="art.svg"> or a CSS background-image is an isolated document. External CSS cannot see into it, JavaScript cannot select its nodes, and a stylesheet rule that works perfectly in a CodePen demo will do nothing at all.
- Inline SVG: CSS, JavaScript, the Web Animations API, hover states, and per-element ids all work.
- Loaded as a file: only what is inside the file runs — SMIL, or CSS in an embedded
<style>block.
If the artwork must animate and must also be cached separately as a file, put the animation inside the file. If it needs to inherit your theme colours or react to a click, it has to be inline. That constraint decides the technique, not aesthetics.
2. Give the thing you want to move a name
Animation targets a selector. A drawing exported as forty anonymous <path> elements gives you nothing to select, which is why so much of the work happens before the first keyframe. Group the artwork by what the parts are, give each group an id, and the CSS writes itself. Animating #stem is possible; animating the fourth path in a flat pile is not.
3. The draw-on effect, and where the number comes from
The best-known SVG animation works by exploiting dashes. Set stroke-dasharray to the full length of the path, so the line becomes a single dash long enough to cover itself. Then push it out of view with stroke-dashoffset and animate that offset back to zero. The stroke appears to draw itself.
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<style>
.draw-line {
stroke-dasharray: 200;
stroke-dashoffset: 200;
animation: draw 2s ease-in-out forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
</style>
<circle cx="50" cy="50" r="30" stroke="#f97316" stroke-width="3" fill="none" class="draw-line" />
</svg>The number 200 above is a guess that happens to be larger than the path, which is the usual quick fix — an over-long dasharray still works, it just makes the easing feel wrong because part of the animation is spent off-screen. For a genuinely correct value, ask the browser for it:
const path = document.querySelector('#stem')
const len = path.getTotalLength() // e.g. 317.42
path.style.strokeDasharray = len
path.style.strokeDashoffset = len
path.getBoundingClientRect() // force a reflow
path.style.transition = 'stroke-dashoffset 2s ease'
path.style.strokeDashoffset = '0'getTotalLength() returns the real length in user units. The forced reflow in the middle is not superstition: without it, the browser may batch the two style changes together and skip the transition entirely.
4. The rotation trap: transform-box
Nearly everyone hits this one. In HTML, transform-origin: center means the centre of the element. In SVG it resolves against the viewBox origin instead, so an icon told to spin around its centre swings around the top-left corner of the canvas and leaves the frame. The fix is one line — transform-box: fill-box — which makes percentages resolve against the element's own bounding box, the way you expected all along.
/* Wrong: rotates around the viewBox origin, so the
icon swings off screen instead of spinning. */
#gear { transform-origin: center; }
/* Right: transform-box makes percentages resolve
against the element's own box. */
#gear {
transform-box: fill-box;
transform-origin: center;
animation: spin 6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }5. SMIL, for files that must animate on their own
SMIL puts the animation inside the markup as elements rather than in a stylesheet, which is its whole reason to exist: it keeps working when the file is loaded through <img>, where CSS cannot reach. Four elements cover most of it — <set>, <animate>, <animateTransform> and <animateMotion>.
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="20" fill="#f97316">
<animate attributeName="r"
values="20; 26; 20"
dur="2s"
repeatCount="indefinite" />
</circle>
<g>
<rect x="44" y="10" width="12" height="12" fill="#111"/>
<animateTransform attributeName="transform"
type="rotate"
from="0 50 50" to="360 50 50"
dur="8s"
repeatCount="indefinite" />
</g>
</svg>Note the from="0 50 50" on the rotation: SMIL takes the centre of rotation as two extra numbers rather than needing transform-box, which is one place it is actually simpler than CSS. The trade is that sequencing anything complicated in SMIL is miserable, and the specification has been formally deprecated for years while browsers keep shipping it. Reach for it when the file has to be self-contained; reach for CSS otherwise.
6. JavaScript, when the animation depends on state
The Web Animations API — element.animate() — gives you real playback control: pause, reverse, seek, and promises that resolve when a run finishes. That is what you want when the motion follows scroll position, a click, or data arriving. The cost is that nothing moves until the script has loaded and run, so it is the wrong choice for anything that should be moving during first paint.
7. What is cheap and what is not
Not all animated properties cost the same. The two that are effectively free are handled by the compositor without touching layout:
- Cheap:
transformandopacity. - Moderate:
stroke-dashoffset,fillandstroke— a repaint, but a contained one. - Expensive:
dfor shape morphing, and geometry attributes likex,yandwidth, which force a re-layout every frame. Prefer a transform.
The thing that actually drops frames, though, is rarely one expensive property. It is animating hundreds of nodes at once — which is a reason to keep the drawing simple before it is a reason to keep the animation simple.
8. Honour reduced motion
Some people get motion sick from looping graphics, and their operating system already knows. Wrapping decorative animation in a prefers-reduced-motion query costs three lines. The part worth getting right is defining the resting state explicitly — switching the animation off without saying where the drawing should sit leaves a half-drawn line on screen forever.
@media (prefers-reduced-motion: reduce) {
.draw-line {
animation: none;
stroke-dashoffset: 0; /* define the resting state */
}
}Where to go next
For choosing between the techniques rather than writing them — plus how SVG animation compares with Lottie, GIF and video, and which tools handle a timeline for you — the SVG animation reference covers the decision. If your artwork is not yet grouped and named, that is the actual prerequisite, and the editor is where it gets fixed.
