Discover SMIL, the often-overlooked way to animate SVGs that works inside <img> tags and can fully animate everything in an SVG without JavaScript.
We know that everything on the web is a box by default, but you’ll find many animated <div>s pretending to be circles. But if you’ve ever met a real <circle>, you’ll know that they’ve got a lot more going for them. Dressed in SVG, they fit into a wider range of crowds than a humble <div> wearing HTML/CSS can. <img> has a strict no .html policy.
The <img> tag is not as static as its name suggests. Any embedded JavaScript unfortunately won’t run if you load an SVG file with an <img> tag, but CSS animations work perfectly fine. Many of the SVG attributes do have CSS property counterparts, and the geometry properties have been supported across the major browsers since 2024. Some attributes that you might want to animate, like viewBox, don’t have equivalents yet.
Besides JavaScript and CSS, there’s another way to animate SVGs: Synchronized Multimedia Integration Language (SMIL). Despite its quirks, it’s still worth learning. Like CSS animations, SMIL animations also work in <img> tags and can fully animate everything in an SVG, without JavaScript.
If you’ve never heard of SMIL or need a refresher, check out Andy Clarke’s well-named article. Then we’ll look at a way to plan an animation and make SMIL markup more manageable.
The Break Up
SMIL has a problem: it gets bloated quickly. Unlike CSS and JavaScript, where you can list multiple properties in each keyframe and easily reuse animations, each SMIL tag can only target one element and only one property of that element at a time. A property can be animated through a list of values. But it is still one tag, one element, one property. The shortest way you can write a color and opacity change that will run is the following:
<animate
attributeName="fill"
to="someOtherColor"
dur="someDuration"
/>
<animate
attributeName="opacity"
to="someOtherValue"
dur="someDuration"
/>
That’s not bad, but consider that it needs to be repeated for every element included in the animation. A SMIL animation can quickly get longer than its CSS equivalent.
To make things easier when starting a new animation, let’s plan all of the elements and properties we want to animate, and create a list of descriptive IDs for each tag.
Charting Animation Time And Space
I like to plan my animations using what’s called a timing chart. A timing chart is effectively a line segment; some choose horizontal lines, others prefer vertical, which is a great analogy for animation as a whole because line segments can run parallel, overlap, and follow each other with or without a gap. Just like animations.
For now, we’re only interested in when animations start and stop. When drawing our charts, we’ll forget about the in-between lines and instead draw a line for each component animation, marking the beginning and end. I like to annotate timing with a circle and a bar. You can draw your chart using whatever tool you prefer, and it doesn’t have to be exactly to scale, as long as the relative timing between all the little animations that make up the whole is clear. Besides, adding labels for the durations is an easy cheat to get around drawing to scale.
Here is a demo of how I typically set up a timing chart with more than one animation:
See the Pen colorAndOpacityChange [forked] by Johan Grobler.
The important thing to note is that the timing chart lines are arranged according to how the animations are arranged in time. One piece of the animation follows the next piece, which is followed by a subsequent piece, and so forth. It visualizes how the animation’s parts run together and cascade over time.
S(yncbase)MIL
A big part of SMIL is synchronization. It’s even in the name, after all. And there are multiple ways to specify when an animation should start (here’s a test case to check what your browser supports). One of the most useful ways is with a syncbase value, which is a SMIL tag’s ID followed by either .begin or .end, with an optional positive or negative offset.
Let’s piggyback off the previous animation example that includes changes in color and opacity. If we want the opacity animation to start 300 milliseconds before the color animation finishes, we could do arithmetic. Alternatively, the second animation can use the syncbase value colorChange.end - 300ms. This way, the relative timing between the two animations becomes explicit.
<!-- Starts at an absolute time -->
<animate
id="colorChange"
begin="1s"
...
/>
<!-- Starts relative to when #first ends -->
<animate
id="opacityChange"
begin="colorChange.end - 300ms"
...
/>
Using syncbase values, the beginning of an animation is positioned in time relative to the .begin or .end of some other animation. A positive offset moves the start to the right (forwards in time), and a negative offset to the left (backwards in time).
See the Pen syncbase.end [forked] by Johan Grobler.
Something worth noting about negative offsets is that they can specify a time before the document has loaded or before a click happens. Computers can’t predict the future (at least not yet). The best they can do is jump the animation to where it would have been had it started earlier. The second animation only runs from start to finish if there is enough room, so to speak.
See the Pen syncbase.begin [forked] by Johan Grobler.
Syncbase values don’t only allow you to connect animations from .end to .begin. Elect a primary animation — the animation that first comes to mind is usually the best representation of the group. All secondary animations can be set with begin="primary.begin". That way, all the other animations begin relative to that starting point. Stacking animations like this reduces maintenance if, say, you later want the whole group to start at a different time.
Let’s put the idea to work and build a loading indicator (or spinner). Then we’re going to explore how changing the relative timing between the parts changes the effect of the whole animation.
Step 1: Choose An Image Approach
Browsers have wide support for the prefers-reduced-motion media feature, and Val Head explains this in depth in another article. Respecting this user preference as we consider moving things around is non-negotiable.
There are various approaches to adhering to a user’s prefers-reduced-motion setting when it comes to SMIL, each with its pros and cons. Evaluating early on what’s going to work best for your use case could save you a partial rewrite down the line.
For example, you could use a <picture> element instead of a plain <img> because <picture> supports multiple <source> elements that can be used as fallbacks via a media attribute for reduced motion preferences.
Another option is one SVG file with an inline CSS @media query that uses display: none to swap between versions. That said, it’s an approach that might cause trouble in various environments, though browsers are continuously improving and this may not be an issue in the future.
You might also consider a CSS background-image instead, since you can wrap that style in a @media (prefers-reduced-motion) query that sets a static image as the fallback for reduced motion preferences.
SVG’s <view> element can also be used to swap things out for motion preferences. Or, if you prefer everything bundled together, you can use JavaScript .matchMedia() and the SMIL DOM interface to control which animations start instead of completely switching out files.
For this tutorial, motion is avoided entirely by sticking to opacity animations, which tend to cause less trouble. For a non-interactive animation like this, loading it in an <img> tag works well. When adding motion, the <picture> route is a reliable way to serve the most appropriate version of the animation to each user.
Step 2: Draw The Graphic
Planning your SVG graphic before writing markup pays dividends when working with SMIL. Sketch out each element you intend to animate, assign it a meaningful ID, and note which properties will change over time. This gives you a clear map to work from when writing your animation tags — and makes it far easier to maintain the markup as the animation grows in complexity.