⭐ If you would like to buy me a coffee, well thank you very much that is mega kind! : https://www.buymeacoffee.com/honeyvig Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Monday, September 7, 2026

Timing Charts: A Blueprint For SMIL Animations

 

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] 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 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] 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] 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] 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] 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] 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] by Johan Grobler.

How about starting the sequence with a fade-out:

See the Pen dotsVersion5 [forked] by Johan Grobler.

You might prefer to start with the dot in the middle:

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

 

Saturday, September 5, 2026

Why Your Website Should Never Stop Changing

 Every website peaks on launch day and slowly drifts from there, not because it breaks, but because nobody has time to keep it current. Autonomous websites, continuously optimized by agents after launch, aim to change that. But take the idea seriously, and you quickly run into a problem that has nothing to do with technology: almost nobody wants a website that changes entirely on its own. we shares what they learned building for full website autonomy and the deeper design problem they uncovered along the way.

Every website is at its best the day it ships. The final branch merges, the site goes live exactly as designed, and it is briefly perfect. It will never be this good again.

Not because anything breaks. The site keeps working. But the market moves, the messaging shifts, a competitor launches something, and the careful thing you built slowly stops matching the company it represents. A year later, it is a period piece. Not broken, just behind. Every team knows this decay, and almost everyone treats it as a law of nature.

It doesn’t have to be that way. A website could keep improving after launch instead of drifting away from its best day, quietly optimizing itself while the team that built it works on something else.

Picture it working: while you sleep, an agent catches that last week’s design-system change never propagated to the pricing page, and fixes it. Another finds a set of images shipped uncompressed in a rushed release and optimizes them. A third flags an accessibility regression a new component introduced, and either fixes it or leaves it for you to check. You wake up to a site that is measurably better than the one you left, and a short list of the few decisions the agents wanted your eyes on. That is the version worth wanting.

And the moment you take that promise seriously, you run into a problem that has nothing to do with the technology:

Almost nobody actually wants a website that changes entirely on its own.

Why “Just Make It Autonomous” Is The Wrong Goal

The obvious move, once you have capable agents, is to hand them the whole site. Let them write, edit, optimize, and publish, and get out of the way. It sounds like the natural endpoint, and it is the first thing most people picture when they hear “autonomous website.”

It is also the thing almost nobody wants once it is real in front of them.

We learned this the way you learn most things worth knowing: by building the opposite first. Building Fimo, an autonomous website platform, we set out to make websites fully autonomous, assumed that was the goal, and then watched what people actually did with it. What they did was hesitate. Not because they distrusted the agents, but because a website has no single owner. Different parts belong to different people, and each one wants a different amount of autonomy. So the question was never whether to trust the agents. It was where to draw the line, and for whom.

Once you ask it that way, the work splits cleanly into three kinds.

Most Of It Is A No-brainer

Start with the largest pile, because it is bigger than people expect. Most of what keeps a website healthy is rule-bound, repetitive, and completely joyless. Keeping accessibility compliant as pages change. Propagating a design-system update once a token moves. Catching a broken meta tag, an unoptimized image, a link that rotted when a URL changed three sprints ago.

None of this is where anyone’s talent lives. Nobody was hired because of their gift for spotting a missing alt attribute. This is the work you are actively relieved to hand off, and it is the work agents are best at, because it is defined by rules rather than taste. An agent that quietly keeps this layer correct across a whole site, unattended, is not a threat to anyone’s job. It is the tedious eighty percent finally taken care of.

Naming how much of the maintenance load actually lives in this pile is what makes the whole idea of an autonomous site feel less like a leap. You are not handing over judgment. You are handing over chores.

Some Of It You’d Never Hand Over 

At the other end sits the work you would not delegate at any price. It is a small pile, but it is the reason you exist.

An agent can check a new page against every rule you have given it. It can confirm the contrast passes, the heading order is right, the tokens are correct, the copy matches the style guide. What it cannot do is decide what the page should feel like, or whether the thing you are shipping is, in the taste sense, good. That judgment is exactly what you were hired for, and no amount of capability moves it off your desk.

This is the part people reach for first when they resist autonomy, and they are right to protect it. The mistake is thinking the whole site is made of this kind of work. Almost none of it is. But that small part matters more than all the rest, which is why automating everything feels so wrong.

And A Lot Of It Depends On Who You Are

Between the chores and the untouchable sits the part no product can settle for you, because the line runs through different places for different people.

Take one real change: making dark mode the default theme when someone lands on the site. An agent can do it in seconds. The question is who gets to decide it should happen at all. For the designer who owns the site’s identity, the default theme is not a setting; it is a statement about how the brand wants to be seen first, and they want that call. For the developer shipping the change, it is a one-line default with a clear rationale, the kind of thing they would happily let an agent apply and move on. Same change, same site, and the two of them draw the line in opposite places.

Notice what is happening there. It isn’t that one of them is cautious and the other reckless. It is that the same task carries different amounts of judgment for each of them. For one person it is a decision; for the other it is a chore. There is no default a product could ship that would be right for both, because “right” is a function of where your value sits, not of the task itself.

This is why the control has to be per-task and per-person, and why we stopped trying to find the setting that would work for everyone. There isn’t one. There is only the line each person draws, and the tools to draw it precisely.

You Don’t Just Set The Autonomy: You Build The Agent

Once you accept that the line is personal, a toggle between “approve” and “delegate” stops being enough. Where the line sits depends on what the agent is actually doing, so the real unit of control is the agent itself.

This is where it stopped being a settings problem, and it is the shape Fimo took. You don’t pick from a fixed menu of behaviors. You compose the agents, deciding what each one is even allowed to touch. You can build one from scratch, or take one close to your needs and shape it to your own line: an accessibility agent you trust to run unattended, a content agent you keep close to anything brand-facing.

Fimo agents, such as content agent, translation agent, asset agent, and so on.

And they don’t stay fixed. They learn from their tasks and from what you teach them, so the boundary you set last month isn’t the one you’re stuck with. What you had to approve then, you can delegate now, not because you lowered your guard but because the agent earned it. The line is not a setting you configure once. It moves as trust is earned, in the direction of less work for you.

Start Narrow, and Widen As You Trust It 

None of this means flipping a site to autonomous on day one. In practice it goes the other way. You delegate a little, watch how it does, and loosen.

And you can actually watch. Every agent’s runs, its history, its logs, and a before-and-after of what it changed are there to inspect. Trust doesn’t grow because you got used to the idea; it grows because you can see what happened and compare. The first time an agent quietly fixes something you would have missed, and you can see exactly what it did, the next delegation gets easier.

Deadlines keep you from becoming the bottleneck on what you have already handed over. If you don’t weigh in, the agent proceeds. You set the terms once, and you stop being the thing the whole site waits on.

The Frozen Site Is The Real Risk

The worry people voice first is that an agent will change something on their site without them. Turn it around: the real risk is a site that never changes at all. A frozen site doesn’t stay safe. It just falls behind, slowly, in a way nobody notices until it represents a company that no longer exists.

The point of autonomy was never to remove you from your website. It was to remove the decay.

The point of autonomy is to keep the launch-day version from being the best version, and to let you spend your judgment on the handful of things that actually deserve it, while the rest takes care of itself. Draw the line where your value is. Let the agents hold everything on the other side of it. And let the line move as they prove they can.