SMIL's Structural Problem

SMIL animations have a scaling issue. Unlike CSS or JavaScript — where you can animate multiple properties on multiple elements in a single rule or loop — each SMIL <animate> tag targets exactly one element and one property. Animating color and opacity on a single element requires two tags:

<animate
  attributeName="fill"
  to="someOtherColor"
  dur="someDuration"
/>

<animate
  attributeName="opacity"
  to="someOtherValue"
  dur="someDuration"
/>

Repeat that for every element in an animation and the markup balloons quickly. Before starting a new SMIL project, it pays to plan out every element and property you intend to animate, complete with descriptive IDs for each tag.

Planning With Timing Charts

Animations are made of parts that run in parallel, overlap, and follow one another. A timing chart turns that temporal relationship into a visual one. The practice, borrowed from traditional animation, draws each component animation as a line segment. You don't need precise scaling — just clear relative positioning — and adding duration labels keeps things readable without graph paper.

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.

Charts reveal how individual animations pile up and cascade over time. When multiple animations share a timeline, a chart shows at a glance where each begins and ends relative to the others.

Syncbase Timing

SMIL's synchronization features are central, which the name Synchronized Multimedia Integration Language hints at. The most useful timing mechanism is the syncbase value: a reference to another animation's ID followed by .begin or .end, optionally adjusted with a positive or negative offset.

Say an opacity animation should start 300 ms before a color animation ends. Instead of manually computing a start time, the opacity animation can declare begin="colorChange.end - 300ms". The relative timing is then explicit in the markup:

<!-- Starts at an absolute time -->
<animate
  id="colorChange"
  begin="1s"
  ...
/>

<!-- Starts relative to when #first ends -->
<animate
  id="opacityChange"
  begin="colorChange.end - 300ms"
  ...
/>

A negative offset begins an animation earlier in time; a positive one pushes it later. Negative offsets come with a limitation worth knowing: pointing before the document has loaded (or before a triggering event has occurred) can't be predicted, so the browser jumps the animation to the state it would have been in had it started on 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.

See the Pen [syncbase.begin [forked]](https://codepen.io/smashingmag/pen/xbgooXq) by Johan Grobler.

See the Pen syncbase.begin [forked] by Johan Grobler.

Syncbases also group animations. Instead of stemming each animation from the document's start time, designate one animation as primary and give the others a begin="primary.begin" value. Changing the primary's start point shifts the entire group, making later timing revisions a one-line change.

To put this into practice, we'll build a three-dot loading spinner, then explore how shifting when each dot fades changes the whole effect.

Step 1: Choose The Delivery Method

Respecting prefers-reduced-motion is non-negotiable, and there are several ways to honor that setting with SMIL. Each has trade-offs; choosing early avoids a partial rewrite later.

A <picture> element can pair a motion version of the SVG with a static fallback in a <source> element using a media attribute. Alternatively, one SVG file could inline a @media (prefers-reduced-motion) query that swaps elements to display: none — though this approach has documented issues in some environments and browsers change over time. A CSS background-image can be wrapped in a media query for the same effect, and even SVG's <view> element can serve static versions. If bundling is preferred, JavaScript's .matchMedia() can toggle SMIL animations through the SVG DOM interface rather than swapping files.

For this project, restricting the animation to opacity avoids the gesture-related pitfalls of moving elements. A non-interactive spinner can load from an <img> tag, but rendering the reduced-motion variant calls for the <picture> approach.

Step 2: Create The Graphics

We're building a 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.

Though an expert can mark up SVG by hand in a text editor, using a graphics application like Inkscape is easier if visualizing the final shape is tricky. One Inkscape gotcha: setting layers does not set element IDs in actual SVG markup but writes to an internal metadata attribute. Elements' true IDs must be set through the object properties or XML editor. Save as optimized SVG before exporting to strip unneeded metadata.

Step 3: Name And Structure The Animation

Sticking with opacity, each dot fades in and back out. Assigning one <animate> tag per dot per transition totals six tags: #fadeInLeft, #fadeInMiddle, #fadeInRight, and matching #fadeOut counterparts.

The fade-in for the left dot:

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

The fade-out for the middle dot:

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

Step 4: Choreograph The Timing

Six animations can be spaced infinitely many ways. These examples use a uniform dur value and syncbase values without offsets, keeping raw timing variables limited. The charts visualize each grouping's effect.

For left-to-right readers, dots fading in across the screen feels natural. The group then fades out together:

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 push each fade-in to follow the previous and restart the loop once all dots are hidden:

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

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

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

Grouped fade-outs ending simultaneously make any one of them a valid restart trigger. Treating #fadeOutLeft as the primary anchor and starting the other fade-outs from fadeOutLeft.begin means retiming the whole group requires changing only one begin attribute:

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

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

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

Other Arrangements

Staggering the fade-outs to mirror the fade-ins changes the flow substantially:

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

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

With no offsets, #fadeOutLeft has two meaningful start points. Beginning 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.

Starting on fadeInMiddle.end shifts everything earlier by a step, producing a different effect:

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.

Moving up again 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.

Starting the entire sequence with a fade-out is another option:

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.

Or opening with the center dot:

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.

Timing charts keep iterations trackable and make comparing versions straightforward. They can also reveal patterns among timing values that markup alone might obscure.

Scaling Up Without Losing the Plot

Adding more moving parts to an animation multiplies the difficulty of keeping every start and end time straight. The same spinner exercise demonstrates this well: the dots in the basic version were animated by manipulating stroke-dashoffset, but a second approach is possible by introducing a <rect> for each dot and using clipping to hide or reveal the stroke. The rectangles get moved over the dots, and the dots’ stroke appears and disappears dynamically rather than through dash offsets:

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.

All three dots share a single <clipPath>; placing it inside a <defs> tag keeps the document organized:

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

Each <circle> then needs the clip path applied, either via CSS or 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)"
/>

Since the dots now carry a stroke, their radius should shrink by half the stroke-width to preserve their visual size:

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

With the markup settled, the animated result and its corresponding timing chart show the new orchestration required:

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.

Reworking the Timeline

A new animation, #moveClipPathLeft, kicks off the sequence. The rhythm also changes: a 1s gap now sits between when the fade-outs finish and when the loop restarts:

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

Moving the rectangles could be done with <animateTransform>, but those tags require returning the rectangles to their starting positions explicitly for a seamless restart. If you animate transform directly, check which data types each SVG tag supports for animation. Here, the middle dot’s <rect> instead moves along the y attribute:

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

The fade-ins also change: they now use fill-opacity rather than opacity, and start right after each dot’s clipping rectangle finishes its movement:

<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 continue to use opacity, so both fill and stroke vanish together. This setup also allows for some markup cleanup. One simplification is dropping fill="freeze" from the dot’s opacity animation, letting it return to its initial value on its own once the animation ends:

<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"
/>

Where fill="freeze" remains necessary, <set> tags can reset those properties. Since these resets have no duration, their start and end markers appear overlapped in the chart. Resetting the left dot’s fill-opacity, for example:

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

If fade-outs keep fill="freeze", then the middle dot needs an extra <set> to restore its opacity:

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

Finally, the clipping rectangles must be moved back into place for the next iteration. The right <rect> gets this reset:

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

This is just one timing scheme among many. The essential pieces for trying alternate variations are already in place; shifting the sequence is a matter of editing time values, not restructuring the SVG.

The Point of a Timing Chart

Multi-step animations, once they go beyond one or two phases, become exercise in orchestration. The markup starts to resemble a Rube Goldberg machine, and keeping track of what runs when is where timing charts help. They function as timelines of expectations, mapping stages to specific moments. That clarity helps both during initial planning and later when maintaining or updating the animation.

A timing chart doesn’t simplify the markup layout itself, but it does offer an at-a-glance reference for sequencing. The technique isn’t exclusive to SMIL either. Syncbase timing values are SMIL-specific, but the visual planning approach carries over to CSS or JavaScript-based animations as well.

For those interested, Andy’s other Smashing Magazine pieces cover CSS and SVG integration in more depth. Yosra Emad’s article on multi-step CSS animations is also useful to read with a pencil and a chart template nearby. Nash Vail’s deeper exploration of easing curves can help when you’re ready to plot intermediate motion lines, and the W3C’s SVG working group maintains a set of browser-support tests worth consulting.

Smashing Editorial