惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
D
Docker
腾讯CDC
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
Martin Fowler
Martin Fowler
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
博客园 - 三生石上(FineUI控件)
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
IT之家
IT之家
WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
爱范儿
爱范儿
Microsoft Azure Blog
Microsoft Azure Blog
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
博客园 - 【当耐特】

Articles on Smashing Magazine — For Web Designers And Developers

Building A UX ROI Case That Survives The Boardroom — Smashing Magazine The Death Of The Button: Why The Best Interface Is No Interface — Smashing Magazine The Many Faces Of September (2026 Wallpapers Edition) — Smashing Magazine Rethinking Data Visualisation: A UX Approach To Dashboards That Actually Drives Decisions — Smashing Magazine Why Your Website Should Never Stop Changing — Smashing Magazine New EU Guidelines For AI Labelling — Smashing Magazine Building Tactile UX: Honoring Intentional Design With Lottie — Smashing Magazine How Baseline Can Help You Ship Less JavaScript — Smashing Magazine Small Joys And Big Adventures (August 2026 Wallpapers Edition) — Smashing Magazine The Bull And Bear Case For Digital Design In The Age Of AI — Smashing Magazine Thinking Outside The Box: Digital Design In The AI Era — Smashing Magazine Weaponizing And Defending The React Flight Protocol: Deserialization Sinks In RSCs — Smashing Magazine When It Makes Sense To “Block” The Main Thread — Smashing Magazine No, People Don’t Want More AI In Their Life — Smashing Magazine From Kickoff To First Concept: How To Turn Brand Strategy Into Visual Direction — Smashing Magazine Designing For Distressed Users: Why Mental Health Apps Shouldn’t Follow Every UI Fashion — Smashing Magazine Meet Kirki: WordPress’s First Visual Builder With An Infinite Canvas — Smashing Magazine Users Don’t Need More Tools: They Need Seamless Integrations — Smashing Magazine Matching AI Modality To User Intent: Designing The Right Interface — Smashing Magazine Why Accessibility Is An Operational Capability, Not A Feature — Smashing Magazine Snapshots Of Summer (July 2026 Wallpapers Edition) — Smashing Magazine Designing With Uncertainty: How AI Supercharges Probabilistic Thinking — Smashing Magazine The Impact Of Humanoid Robots On Humanity — Smashing Magazine The Benefits Of Cognitive Inclusion In UX Research — Smashing Magazine How To Make Your Design System AI-Ready — Smashing Magazine June Is For Exploring (2026 Wallpapers Edition) — Smashing Magazine Algorithmic Theming Engines: Building Self-Correcting Color Systems With contrast-color() — Smashing Magazine Your Prototype Is Not Being Honest With Your Users (And Here’s How To Fix It) — Smashing Magazine Four Levels Of Customer Understanding — Smashing Magazine Advanced Tree Counting: Mathematical Layouts With sibling-index() And sibling-count() — Smashing Magazine
Timing Charts: A Blueprint For SMIL Animations — Smashing...
https://www.smashingmagazine.com/author/johan-grobler/ · 2026-08-20 · via Articles on Smashing Magazine — For Web Designers And Developers

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, 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.

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]](https://codepen.io/smashingmag/pen/emgwwGJ) by Johan Grobler.

See the Pen syncbase.end [forked] 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.

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". 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 <picture> element instead of a plain <img> because <picture> supports multiple <source> 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 <view> 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 <img> tag. When we add motion, we can go the <picture> 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.

See the Pen StaticDots [forked] 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 <animate> 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 <animate> tag for the dot on the left:

<animate
  id="fadeInLeft"
  href="#leftDot"
  attributeName="opacity"
  from="0"
  to="1"
  ...
/>

And the fade-out <animate> for the middle dot:

<animate
  id="fadeOutMiddle"
  href="#middleDot"
  attributeName="opacity"
  from="1"
  to="0"
  ...
/>

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 <animate> 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.

See the Pen dotsVersion1 [forked] by Johan Grobler.

Syncbase values stagger the fade-ins and restart the loop when the dots have disappeared:

<animate
  id="fadeInLeft"
  ...
  begin="0s; fadeOutLeft.end"
/>

<animate
  id="fadeInMiddle"
  ...
  begin="fadeInLeft.end"
/>

<animate
  id="fadeInRight"
  ...
  begin="fadeInMiddle.end"
/>

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.

<animate
  id="fadeOutLeft"
  ...
  begin="fadeInRight.end"
/>

<animate
  id="fadeOutMiddle"
  ...
  begin="fadeOutLeft.begin"
/>

<animate
  id="fadeOutRight"
  ...
  begin="fadeOutLeft.begin"
/>

Some Alternate Timings

Instead of a group fade-out, we could stagger them just like the fade-ins:

<animate
  id="fadeOutMiddle"
  ...
  begin="fadeOutLeft.end"
/>

<animate
  id="fadeOutRight"
  ...
  begin="fadeOutMiddle.end"
/>

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.

See the Pen dotsVersion2 [forked] 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.

See the Pen dotsVersion3 [forked] 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.

See the Pen dotsVersion4 [forked] 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.

See the Pen dotsVersion5 [forked] 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.

See the Pen centerFirstDots [forked] 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.

See the Pen staticDotsWithClipPaths [forked] by Johan Grobler.

I’ve added a <rect> for each dot to the drawing. We’ll move those into to a <cilpPath> 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 <clipPath> for all three dots, but it adds structure to the document, and it’s good practice to wrap it, and similar tags, in a <defs> tag:

<defs>
  <clipPath id="dotsClipPath">
  <!-- The geometry of the rectangles and coordinates used here, and later, depends on the viewBox used for their parent <svg> element. -->
    <rect
      id="clipPathLeftRect"
      width="2" height="2"
      x="1" y="6"
    />
    <rect
      id="clipPathMiddleRect"
      width="2" height="2"
      x="4" y="2"
    />
    <rect
      id="clipPathRightRect"
      width="2" height="2"
      x="7" y="6">
  </clipPath>
</defs>

Remember to set the clip path for the <circle>s. Either with CSS or using the clip-path attribute:

<circle
  id="leftDot"
  ...
  clip-path="url(#dotsClipPath)"
/>

<circle
  id="middleDot"
  ...
  clip-path="url(#dotsClipPath)"
  />

<circle
  id="rightDot"
  ...
  clip-path="url(#dotsClipPath)"
/>

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:

<circle
  ...
  r="0.9"
  stroke-width="0.2"
  ...
/>

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.

See the Pen clipPathDots [forked] 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:

<animate
  id="moveClipPathLeft"
  href="#clipPathLeftRect"
  attributeName="y"
  from="6"
  to="4"
  begin="0s; fadeOutLeft.end + 1s"
  fill="freeze"
/>

You could use <animateTransform>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 <rect> for the middle dot moves down:

<animate
  id="moveClipPathMiddle"
  href="#clipPathMiddleRect"
  attributeName="y"
  from="2"
  to="4"
  begin="moveClipPathLeft.end"
  fill="freeze"
/>

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 <rect> has finished moving:

<animate
  id="fadeInLeft"
  href="#leftDot"
  attributeName="fill-opacity"
  to="1"
  dur="1s"
  begin="moveClipPathLeft.end"
  fill="freeze"
/>

<animate
  id="fadeInMiddle"
  href="#middleDot"
  ...
  begin="moveClipPathMiddle.end"
  ...
/>

<animate
  id="fadeInRight"
  href="#rightDot"
  ...
  begin="moveClipPathRight.end"
  ...
/>

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:

<animate
  id="fadeOutLeft"
  href="#leftDot"
  attributeName="opacity"
  to="0"
  dur="1s"
  begin="fadeInRight.end"
/>

<!-- We'll still consider #fadeOutLeft as the primary animation here and sync the start of the others to it. -->

<animate
  id="fadeOutMiddle"
  href="#middleDot"
  ...
  begin="fadeOutLeft.begin"
/>

<animate
  id="fadeOutRight"
  href="#rightDot"
  ...
  begin="fadeOutLeft.begin"
/>

For the <animate> tags that did use fill="freeze", we’ll use <set> tags to reset those properties back to their starting values. I’ve simplified the chart a little by lumping those tags together. Because these <set> 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:

<set
  href="#leftDot"
  attributeName="fill-opacity"
  to="0"
  begin="fadeOutLeft.end"
  fill="freeze"
/>

If you use fill="freeze" with the fade-outs, you’ll need an extra <set> for each dot to reset the middle dot’s opacity:

<set
  href="#middleDot"
  attributeName="opacity"
  to="1"
  begin="fadeOutLeft.end"
  fill="freeze"
/>

The last thing we need to do is move the clipping rectangles back to their starting positions. For the right <rect>:

<set
  href="#clipPathRightRect"
  attributeName="y"
  to="6"
  begin="fadeOutLeft.end"
  fill="freeze"
/>

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.

Smashing Editorial (gg, yk)