![]()
has a strict no .html policy.
The tag is not as static as its name suggests. Any embedded JavaScript unfortunately won’t run if you load an SVG file with an 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 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:
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, 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]](https://codepen.io/smashingmag/pen/01a016dc-399c-7652-b172-4cd4663746b9) 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.
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]](https://codepen.io/smashingmag/pen/emgwwGJ) by Johan Grobler.
Something with negative offsets is that they can specify a time before the document has loaded or when 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 the computer peeked into the future to preemptively start the animation. The second animation only runs from start to finish if there is enough room, so to speak.
See the Pen [syncbase.begin [forked]](https://codepen.io/smashingmag/pen/xbgooXq) 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". I’ve only used the ID #primary for emphasis. That way, all the other animations begin relative to that starting point. Stacking animations like this reduces maintenance if, say, we 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. We definitely want to respect this user preference as we consider moving things around. In fact, consider it 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, we could consider using a element instead of a plain because supports multiple elements that can be used as fallbacks in a media attribute for reduced motion preferences.
Or 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. But browsers are continuously changing, and this might not be an issue in the future.
You might also consider a CSS background-image instead because we can wrap that style in a media query — @media (prefers-reduced-motion) — that sets a static image as the fallback for reduced motion preferences.
There are even more options we can turn to! For example, SVG’s element can also be used to swap things out for motion preferences.
Or, if we prefer everything bundled together, we can use JavaScript .matchMedia() and the handy SMIL DOM interface to control which animations start instead of completely switching out files.
For this, I’m avoiding any motion and sticking to opacity animations, which tend to cause less trouble. For a non-interactive animation like this, we can load it in an tag. When we add motion, we can go the route to show the most appropriate version of our animation.
Step 2: Draw The Graphics
We’re going to do our own version of the classic three-dot spinner:
See the Pen [StaticDots [forked]](https://codepen.io/smashingmag/pen/01a016e5-c44d-75b3-bbb2-3eca2f83932d) by Johan Grobler.
SVG wizards might be able to do everything directly in a text editor. I recommend using a graphic editor like Inkscape if you’re having trouble visualizing how the markup will be rendered. Once again, Andy Clarke has a great article about his process for optimizing and structuring his own drawings.
Note: There’s a gotcha with Inkscape. Setting what you would expect to be an element’s ID via the Layers window actually sets the value of a metadata attribute used internally by Inkscape. Use Inkscape’s object properties or XML editor window to set the true element’s ID. Your mileage may vary with a different editor. Also, in Inkscape, remember to save the file as optimized SVG when the drawing is done to strip away unneeded metadata.
Step 3: Outline The Animation
OK, so we’re sticking with the opacity animation idea. The dots are going to fade in and out. We’ll use separate tags for those. Six tags in total.
Our naming scheme is going to be straightforward: we’ll call them #fadeIn and #fadeOut, and to differentiate between each pair of tags, we’ll postfix the tag’s ID with either Left, Middle or Right. Try to follow a convention that makes sense to you when coming up with your own IDs.
The fade-in tag for the dot on the left:
And the fade-out for the middle dot:
Step 4: Time The Animations
We have an infinite number of ways in which we could space these six animations in time. Let’s look at a couple of choice examples alongside their timing charts to see how changing the arrangement of the parts impacts the visual effect of the whole animation.
To narrow our choices a bit, all of the tags will use the same dur value, and none of the syncbase values will have offsets.
For someone coming from a culture that reads from left to right, the dots appearing on screen along the same pattern would feel natural. Let’s also start with all the dots fading out together at the end:
See the Pen [dotsVersion1 [forked]](https://codepen.io/smashingmag/pen/01a016ec-0b1b-731e-bf44-2dd45b7c075f) by Johan Grobler.
Syncbase values stagger the fade-ins and restart the loop when the dots have disappeared:
Since all the fade-outs end at the same time, it is an arbitrary choice which one we use to restart the loop. We’ll consider #fadeOutLeft as the primary animation here and also synchronize the other fade-outs to it with the syncbase value fadeOutLeft.begin. Later, if we want to move the fade-outs in time, all we do is change when #fadeOutLeft starts.
Some Alternate Timings
Instead of a group fade-out, we could stagger them just like the fade-ins:
Without adding an offset, there are a couple of points in time we could start #fadeOutLeft at. If it starts on fadeInRight.end:
See the Pen [dotsVersion2 [forked]](https://codepen.io/smashingmag/pen/01a016ef-1be0-7359-a8ce-1d45194ed50a) by Johan Grobler.
The visual effect is subtly changed by moving up a spot and starting #fadeOutLeft on fadeInMiddle.end instead:
See the Pen [dotsVersion3 [forked]](https://codepen.io/smashingmag/pen/01a016ef-f4d9-745b-a89f-2ff35d898589) by Johan Grobler.
We can see from the charts that we could try moving up a spot further to fadeInLeft.end:
See the Pen [dotsVersion4 [forked]](https://codepen.io/smashingmag/pen/01a016f1-07f8-732a-966d-c295618c60d7) by Johan Grobler.
How about starting the sequence with a fade-out:
See the Pen [dotsVersion5 [forked]](https://codepen.io/smashingmag/pen/01a016f1-b6a5-7078-a73c-217459f53e20) by Johan Grobler.
You might prefer to start with the dot in the middle:
See the Pen [centerFirstDots [forked]](https://codepen.io/smashingmag/pen/01a016f2-5f78-70a2-a948-f030c147fbe3) by Johan Grobler.
As you iterate on your animation, timing charts are a great way to keep track of your work, and they make visual comparison between versions possible. And by drawing a timing chart, you might even see a pattern in the timing between the parts of the animations that you might otherwise have missed.
Step 5: Adding More Animations
As you animate more elements and properties, it gets harder to keep track of what starts when. To see how timing charts can help you make sense of things, let’s build on the basic spinner:
See the Pen [staticDotsWithClipPaths [forked]](https://codepen.io/smashingmag/pen/01a016f4-f99c-7779-b526-1265ecf5619b) by Johan Grobler.
I’ve added a for each dot to the drawing. We’ll move those into to a tag and remove the fill="white". As the animation runs, the rectangles are going to move over the dots for a different approach to animating the stroke than by animating stroke-dashoffset.
We only need a single for all three dots, but it adds structure to the document, and it’s good practice to wrap it, and similar tags, in a tag:
Remember to set the clip path for the s. Either with CSS or using the clip-path attribute:
Because the dots now have a stroke added, to leave their size unchanged, we need to compensate by subtracting half the value of stroke-width from r:
That’s all the changes the graphics need. Have a look at the animated version with its timing chart, then we’ll look in more detail at the changes that have been made to the animation:
See the Pen [clipPathDots [forked]](https://codepen.io/smashingmag/pen/01a016f8-73b0-707a-b21a-7aa07fb194b2) by Johan Grobler.
There’s a new animation, #moveClipPathLeft, that starts the whole sequence, and to tweak the animation’s rhythm a bit, there’s a 1s delay between when the fade-outs end and the loop restarts:
You could use s to move the rectangles instead, but we need to move them back to their starting positions at the end for a smooth restart of the animation. You’ll need to take into account which tags can animate and set which data types if you do decide to animate the transform attribute instead of translating the rectangles with the y attribute.
To change things up, the for the middle dot moves down:
We’re also using the fill-opacity property instead of the opacity property for the fade-ins, and they’ve been synchronized to start once a dot’s clipping has finished moving:
The fade-outs still use the normal opacity property, so both the fill and stroke fade out together. Because this arrangement is set up to use a group fade-out at the end, there are a few places the markup could be optimized. One of them is dropping the fill="freeze" to automatically reset the dot’s opacity back to its starting value once the tag finishes running:
For the tags that did use fill="freeze", we’ll use tags to reset those properties back to their starting values. I’ve simplified the chart a little by lumping those tags together. Because these tags don’t have a duration over which they act, on the chart I’ve drawn the start and end markers over each other.
To reset the fill-opacity on the left dot:
If you use fill="freeze" with the fade-outs, you’ll need an extra for each dot to reset the middle dot’s opacity:
The last thing we need to do is move the clipping rectangles back to their starting positions. For the right :
That’s just one of the possible timing variations, and most of what you’ll need to try some of the others, as we did with the basic spinner, is already in place. You might want to have a try at coming up with a couple of your own alternate timings.
That’s The Benefit Of Timing Charts
To sum things up, we know that balancing the different stages of an animation can be difficult at best, and untenable at worst. Any time we get into multi-step animations that exceed one or two steps, it’s a form of orchestration. You’re almost building a Rube Goldberg machine of markup. And using a timing chart is a strategy I use that I hope will help you in your projects as well. They are outlines of what to expect and when, allowing you to map things out in a way that not only helps plan your code, but also makes future updates and maintenance a lot more bearable than going into it without a plan.
While a timing chart can’t reduce the complexity of the markup, it can give a good overview of what should happen when. Timing charts are definitely not limited to SMIL animations. Unfortunately, syncbase values are, and they can still help even if you’re going to use a different approach to implementing your animation.
Further Reading
I highly recommend checking out Andy’s other articles on Smashing Magazine. There’s even more stuff on combining CSS and SVG.
Yosra Emad wrote a great article on creating animations with multiple steps in CSS, and you might want to try drawing a few timing charts as you read through it.
When you’re ready to start adding those in-between lines to your timing chart, Nash Vail’s article on easing dives deep into the details of easing curves.
You might also be interested in tests for your browser’s SVG implementation to see what is supported.
(gg, yk)
