Lesson 10
SMIL Advanced
Lesson 9 covered <animate> and <set> - animating individual attributes with timing, easing, and repetition. This lesson completes the SMIL picture with the three remaining animation elements and the orchestration patterns that make complex, choreographed animations possible.
1. <animateTransform>
You can't animate the transform attribute with <animate> because transform values aren't simple numbers - they're function calls like rotate(45, 100, 100). The <animateTransform> element exists specifically for this. It takes a type attribute specifying which transform function to animate:
| type | from/to format | What it does |
|---|---|---|
translate | "tx ty" | Moves the element |
rotate | "angle cx cy" | Rotates angle degrees around point (cx, cy) |
scale | "sx sy" | Scales (sy defaults to sx if omitted) |
skewX | "angle" | Horizontal skew |
skewY | "angle" | Vertical skew |
You might expect matrix to be a valid type, but it's not. The spec deliberately excludes it because the browser can't meaningfully interpolate between two arbitrary 6-value matrices - raw number interpolation produces weird non-linear warping (shearing mid-animation rather than smooth rotation or scaling). CSS solves this by decomposing matrices into individual functions before interpolating; SMIL doesn't do that decomposition. Instead, compose multiple <animateTransform> elements (one per function) with additive="sum". If you genuinely need arbitrary matrix interpolation, that's JavaScript territory (Web Animations API).
Rotation
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect x="75" y="75" width="50" height="50" fill="coral">
<animateTransform
attributeName="transform"
type="rotate"
from="0 100 100"
to="360 100 100"
dur="3s"
repeatCount="indefinite"/>
</rect>
</svg>
<!-- "0 100 100" means: 0 degrees rotation, centred on point (100,100)
"360 100 100" means: 360 degrees rotation, same centre -->
Continuous rotation around the SVG centre (100,100). The from/to values include the rotation centre.
Unlike CSS transform-origin (which is set separately), SMIL's rotate type includes the centre coordinates in every value: "angle cx cy". This means you can even animate the rotation centre itself by changing cx/cy across keyframes - something CSS can't do mid-animation.
Scale (pulse effect)
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Group to set the scaling origin via translate -->
<g transform="translate(100, 100)">
<circle cx="0" cy="0" r="30" fill="#61afef">
<animateTransform
attributeName="transform"
type="scale"
values="1;1.4;1"
dur="2s"
repeatCount="indefinite"/>
</circle>
</g>
</svg>
Scale pulse. The element is centred at (0,0) inside a translated <g> so scaling happens from the centre rather than the top-left.
type="scale" scales from the coordinate system origin (0,0). If your element is at (100,100), it will fly away from the corner as it scales. The workaround: wrap in a <g transform="translate(cx, cy)"> and position the element at (0,0) within that group. Then scale happens from the element's visual centre.
Combining multiple transforms
By default, <animateTransform> replaces the entire transform attribute each frame. If you have two <animateTransform> elements on the same element, the second overwrites the first (not composing them). To stack transforms, set additive="sum" on the second one:
<rect x="-25" y="-25" width="50" height="50" fill="coral">
<!-- First: rotate around origin -->
<animateTransform attributeName="transform" type="rotate"
from="0" to="360" dur="4s" repeatCount="indefinite"/>
<!-- Second: scale pulse (additive="sum" to compose) -->
<animateTransform attributeName="transform" type="scale"
values="1;1.3;1" dur="1.5s" repeatCount="indefinite"
additive="sum"/>
</rect>
Rotation + scale pulse composed via additive="sum". Without it, only the scale would run (it would replace the rotation).
This is where additive (from Lesson 9) becomes critical. Without it, multiple <animateTransform> elements fight over the same transform attribute and the last one wins. With additive="sum", they concatenate - exactly like writing transform="rotate(...) scale(...)" as a static attribute.
2. <animateMotion>
<animateMotion> moves an element along a path. Unlike animating cx/cy (which only gives you straight lines between keyframes), <animateMotion> follows arbitrary curves - Béziers, arcs, anything you can express as path data.
| Attribute | Purpose | Example |
|---|---|---|
path | The motion path (same syntax as <path d="...">) | "M0,0 C50,-50 100,50 150,0" |
rotate | How the element orients along the path | "auto", "auto-reverse", or a fixed angle |
keyPoints | Map keyTimes to positions along the path (0–1) | "0;0.3;1" |
calcMode | Same as <animate> (linear, paced, spline, discrete) | "paced" |
Basic motion along a curve
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<!-- Show the path for reference -->
<path d="M 50,150 C 100,20 300,20 350,150"
fill="none" stroke="grey" stroke-dasharray="4 2"/>
<circle r="10" fill="coral">
<animateMotion
path="M 50,150 C 100,20 300,20 350,150"
dur="3s"
repeatCount="indefinite"/>
</circle>
</svg>
A circle following a cubic Bézier arc. The path is shown dashed for reference.
rotate="auto" - orienting along the path
By default, the element doesn't rotate as it moves - it just translates. Set rotate="auto" to make it orient tangent to the path (like a car following a road):
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<path d="M 50,100 C 100,20 200,180 250,100 S 350,20 380,100"
fill="none" stroke="grey" stroke-dasharray="4 2"/>
<!-- Arrow shape that rotates to follow the path -->
<polygon points="-8,-5 8,0 -8,5" fill="coral">
<animateMotion
path="M 50,100 C 100,20 200,180 250,100 S 350,20 380,100"
dur="4s"
rotate="auto"
repeatCount="indefinite"/>
</polygon>
</svg>
An arrow following an S-curve with rotate="auto". The arrow always points in the direction of travel.
| rotate value | Behaviour |
|---|---|
auto | Element rotates to face the direction of motion (tangent to path) |
auto-reverse | Same as auto but flipped 180° (pointing backwards) |
0 (or any number) | Fixed rotation angle - element doesn't turn as it moves |
Using <mpath> to reference an existing path
Instead of duplicating path data in the path attribute, you can reference a <path> element defined elsewhere using <mpath>:
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="track" d="M 30,100 Q 200,20 370,100"/>
</defs>
<!-- Show the track -->
<use href="#track" fill="none" stroke="grey" stroke-dasharray="4 2"/>
<circle r="8" fill="#61afef">
<animateMotion dur="3s" repeatCount="indefinite">
<mpath href="#track"/>
</animateMotion>
</circle>
</svg>
<mpath href="#track"/> reuses an existing path definition. Single source of truth - change the path once, both the visual and the motion update.
Unlike <animate> (which defaults to linear), <animateMotion> defaults to paced. This means the element moves at constant velocity along the path regardless of how the control points are spaced. You don't need to set it explicitly - but be aware that adding keyTimes requires switching to calcMode="linear" or "spline" since paced ignores keyTimes.
3. Sequencing Animations (Reliable Patterns)
In Lesson 9, we discovered that the spec's syncbase timing (begin="animId.end") and cross-element event references (begin="otherId.click") don't work reliably in Chrome. This section covers the patterns that do work across browsers.
Pattern 1: Offset from document load
The simplest approach - stagger animations with absolute time offsets:
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="15" fill="coral">
<animate attributeName="cx" from="50" to="350"
dur="1s" begin="0s" fill="freeze"/>
</circle>
<circle cx="50" cy="50" r="15" fill="#61afef">
<animate attributeName="cx" from="50" to="350"
dur="1s" begin="1s" fill="freeze"/>
</circle>
<circle cx="50" cy="50" r="15" fill="#98c379">
<animate attributeName="cx" from="50" to="350"
dur="1s" begin="2s" fill="freeze"/>
</circle>
</svg>
Three circles staggered by 1s. Simple, reliable, works everywhere. Downside: if you change one duration, you must recalculate all offsets.
Pattern 2: Offset from a shared event
When the sequence should be user-triggered, offset all animations from the same click event:
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<g style="cursor:pointer">
<circle cx="50" cy="50" r="20" fill="coral">
<animate attributeName="cx" from="50" to="350"
dur="1s" begin="click" fill="freeze"/>
<animate attributeName="r" from="20" to="10"
dur="0.5s" begin="click+1s" fill="freeze"/>
<animate attributeName="opacity" from="1" to="0.3"
dur="0.5s" begin="click+1.5s" fill="freeze"/>
</circle>
</g>
</svg>
Three-step sequence from one click. Each begin is offset from the same click event by the cumulative duration of prior steps.
Pattern 3: Looping sequences with begin lists
The begin attribute accepts a semicolon-separated list of start times. This lets you restart an animation at multiple points - useful for creating loops that include pauses:
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="15" fill="coral">
<!-- Runs at 0s, then again at 4s, then again at 8s... -->
<animate attributeName="cx" from="50" to="350"
dur="2s" fill="freeze"
begin="0s; 4s; 8s; 12s"/>
<!-- Snap back at 3s, 7s, 11s, 15s (between runs) -->
<set attributeName="cx" to="50"
begin="3s; 7s; 11s; 15s"/>
</circle>
</svg>
The circle slides right every 4 seconds, with a 1-second pause before snapping back. Each start time is explicitly listed in begin.
Since syncbase timing is unreliable in browsers, begin lists are the most pragmatic way to schedule complex choreography. You explicitly list every start time. It's verbose but completely reliable. For simpler cases, a single begin offset with repeatCount is sufficient.
4. Event-Based Triggers
Beyond begin="click", SMIL supports several event types. The reliable pattern (from our Lesson 9 testing) is: the animation must be inside the element that receives the event.
| Event | Fires when | Works in Chrome? |
|---|---|---|
click | Element is clicked | Yes (on parent element) |
mouseover | Cursor enters the element | Yes (on parent element) |
mouseout | Cursor leaves the element | Yes (on parent element) |
mousedown | Mouse button pressed on element | Yes (on parent element) |
mouseup | Mouse button released on element | Yes (on parent element) |
focusin | Element receives keyboard focus (requires tabindex attribute on the element) | Yes |
Hover effect with SMIL
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="40" fill="coral" style="cursor:pointer">
<!-- Grow on hover -->
<animate attributeName="r" from="40" to="55"
dur="0.3s" begin="mouseover" fill="freeze"/>
<!-- Shrink back on mouse leave -->
<animate attributeName="r" from="55" to="40"
dur="0.3s" begin="mouseout" fill="freeze"/>
</circle>
</svg>
Hover over the circle - it grows. Move away - it shrinks. Pure SMIL hover effect, no CSS transitions needed.
For simple hover effects on CSS-animatable properties (opacity, transform), CSS :hover transitions are simpler and GPU-accelerated. SMIL hover is useful when you need to animate attributes CSS can't reach (like r, d, filter parameters) or want hover effects on standalone SVG files loaded via <img> (where CSS hover doesn't work because there's no external stylesheet). Note: <img> doesn't propagate mouse events, so SMIL hover won't work there either - use <object> or inline SVG.
5. Animating the viewBox (Zoom & Pan)
One of SMIL's unique capabilities - animating the viewBox attribute creates zoom and pan effects without changing any element's position:
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg"
style="cursor:pointer">
<!-- Scene content -->
<circle cx="100" cy="100" r="20" fill="coral"/>
<circle cx="300" cy="100" r="20" fill="#61afef"/>
<rect x="180" y="80" width="40" height="40" fill="#98c379"/>
<!-- Zoom into the green rect on click -->
<animate attributeName="viewBox"
from="0 0 400 200"
to="160 60 80 80"
dur="1.5s" begin="click" fill="freeze"/>
</svg>
Click the SVG - the viewBox narrows to frame the green square, creating a smooth zoom effect. No element moves; only the camera does.
Animating viewBox is like moving a camera over your scene. The four values are min-x min-y width height. Decreasing width/height zooms in; shifting min-x/min-y pans. This is uniquely powerful for storytelling animations - guiding the viewer's attention through a complex scene without moving any elements.
6. Practical Example: Orbiting Satellites
Combining <animateMotion>, <animateTransform>, and polyrhythmic timing for an organic orbital animation:
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="orbit1" d="M 150,50 A 100,100 0 1 1 149.99,50" fill="none"/>
<path id="orbit2" d="M 150,80 A 70,70 0 1 1 149.99,80" fill="none"/>
</defs>
<!-- Show orbits -->
<use href="#orbit1" stroke="grey" stroke-dasharray="2 4" fill="none"/>
<use href="#orbit2" stroke="grey" stroke-dasharray="2 4" fill="none"/>
<!-- Central body -->
<circle cx="150" cy="150" r="20" fill="#e5c07b"/>
<!-- Satellite 1: outer orbit, 6s period -->
<circle r="8" fill="coral">
<animateMotion dur="6s" repeatCount="indefinite">
<mpath href="#orbit1"/>
</animateMotion>
</circle>
<!-- Satellite 2: inner orbit, 4s period, two-tone -->
<g>
<circle r="6" fill="#61afef"/>
<path d="M 0,-6 A 6,6 0 0 1 0,6 Z" fill="#1a4a7a"/>
<animateMotion dur="4s" repeatCount="indefinite">
<mpath href="#orbit2"/>
</animateMotion>
</g>
</svg>
Two satellites: coral circle on the outer orbit (6s), blue square on the inner orbit (4s) spinning via additive="sum" (1.5s rotation). The square's corners make the spin clearly visible.
7. Practical Example: Stroke Drawing with Reveal
The classic "line drawing" animation - a path appears to draw itself, then a fill fades in:
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<path d="M 40,160 Q 100,20 160,160" fill="none"
stroke="coral" stroke-width="3"
stroke-dasharray="300" stroke-dashoffset="300">
<!-- Draw the stroke -->
<animate attributeName="stroke-dashoffset"
from="300" to="0" dur="2s" fill="freeze"/>
<!-- Then fill fades in -->
<set attributeName="fill" to="rgba(255,127,80,0.2)"
begin="2s" fill="freeze"/>
</path>
</svg>
The path draws itself over 2 seconds (stroke-dashoffset animates from full to 0), then a translucent fill appears. This is the technique behind SVG logo animations.
Set stroke-dasharray equal to (or larger than) the path's total length, and stroke-dashoffset to the same value - this hides the stroke entirely. Animate stroke-dashoffset to 0 to "draw" it. You can find a path's length with pathElement.getTotalLength() in JS, or estimate it visually. This technique is covered in more depth in Lesson 11 (CSS animation of SVG).
8. Summary: The Four SMIL Animation Elements
| Element | Purpose | Key attributes |
|---|---|---|
<animate> | Animate any single attribute over time | attributeName, from/to/values, calcMode |
<set> | Discrete value change at a point in time | attributeName, to, begin |
<animateTransform> | Animate the transform attribute | type (rotate/scale/translate/skew), additive |
<animateMotion> | Move element along a path | path or <mpath>, rotate |
All four share the same timing attributes: dur, begin, repeatCount, repeatDur, fill, calcMode, keyTimes, keySplines, additive, accumulate.
Quiz
Question 1
In <animateTransform type="rotate" from="0 100 100" to="360 100 100">, what do the 100 100 numbers represent?
Question 2
What happens if you have two <animateTransform> elements on one element without additive="sum"?
Question 3
What does rotate="auto" on <animateMotion> do?
Question 4
What is the default calcMode for <animateMotion>?
Question 5
What does animating the viewBox attribute achieve?
Exercises
- Spinning loader - Create a circle with a dashed stroke that rotates continuously using
<animateTransform type="rotate">. Combine with stroke-dashoffset animation for the "material design" spinner effect. - Following a figure-8 - Draw a figure-8 path and use
<animateMotion>withrotate="auto"to move an arrow along it. Use<mpath>to reference the path. - Composed transforms - Create a rectangle that simultaneously rotates (4s) and pulses its scale (1.5s) using two
<animateTransform>elements withadditive="sum". - Click-triggered sequence - Build a 3-step animation triggered by click: (1) element moves right, (2) changes colour, (3) fades out. Use
begin="click",begin="click+Xs"pattern. - Zoom storytelling - Create a scene with 3 objects at different positions. Animate
viewBoxthrough a sequence of keyframes that zooms into each object in turn, pausing on each one. UsevalueswithkeyTimes.