Lesson 9
SMIL Animation Fundamentals
SMIL (Synchronized Multimedia Integration Language) gives SVG a declarative animation system that lives inside the markup itself. No JavaScript, no external CSS files - the animation is part of the SVG document. While CSS @keyframes inside a <style> block can also produce self-contained animations, SMIL can target any SVG attribute - including geometry (cx, d, viewBox), filter parameters (stdDeviation), and text attributes (startOffset) that CSS simply can't reach. For truly self-contained animated SVGs that go beyond what CSS properties allow, SMIL is the tool.
This lesson covers the core <animate> element and the timing model that drives it. Lesson 10 will build on this with <animateTransform>, <animateMotion>, and advanced timing (syncbase, events, orchestration).
1. The Mental Model
A SMIL animation is a child element nested inside the element it animates. It declares: "animate this attribute, from this value to that value, over this duration." The browser handles interpolation.
Unlike CSS @keyframes (which are defined globally and applied via class), SMIL animations live inside the element they affect. This is what makes them self-contained - move or copy the element, and its animation comes with it. The animation element overrides the attribute value for its duration, then (by default) the attribute snaps back to its original value.
<circle cx="50" cy="50" r="20" fill="coral">
<!-- This animate element is a CHILD of the circle -->
<animate
attributeName="r"
from="20"
to="40"
dur="2s"
repeatCount="indefinite"/>
</circle>
A pulsing circle - the simplest possible SMIL animation. The radius animates from 20 to 40, looping indefinitely.
2. The <animate> Element
<animate> is the workhorse. It animates a single attribute of its parent element over time. Here are its core attributes:
| Attribute | Purpose | Example |
|---|---|---|
attributeName | Which attribute to animate | "cx", "fill", "opacity" |
from | Starting value | "0" |
to | Ending value | "100" |
by | Relative change (alternative to to) | "50" (adds 50 to current) |
values | Semicolon-separated keyframe values (overrides from/to) | "0;100;50" |
dur | Duration (clock value) | "3s", "500ms", "0:02:30" |
begin | When to start | "0s", "2s", "click" |
repeatCount | How many times to repeat | "3", "indefinite" |
repeatDur | Total time to keep repeating | "10s" |
fill | What happens when animation ends | "freeze" or "remove" |
The fill attribute on an animation element means something completely different from fill on a shape. On <animate>, it controls whether the final animated value is held (freeze) or removed (remove, the default - snaps back to the original). This is a common source of confusion.
from/to vs values
You can define an animation two ways:
from/to- simple A-to-B transitionvalues- multiple keyframes, semicolon-separated. The animation progresses through them evenly (unless you usekeyTimes)
<!-- Two-value animation (from/to) -->
<rect width="50" height="50" fill="coral">
<animate attributeName="x" from="0" to="300" dur="3s"
repeatCount="indefinite"/>
</rect>
<!-- Multi-value animation (values) -->
<rect width="50" height="50" fill="#61afef">
<animate attributeName="x" values="0;300;150;300;0" dur="4s"
repeatCount="indefinite"/>
</rect>
Top: simple linear motion. Bottom: multi-keyframe path creating a bouncing pattern.
3. Timing: dur, begin, repeatCount
Clock values (dur)
Duration uses clock-value syntax:
| Format | Example | Meaning |
|---|---|---|
| Seconds | 3s | 3 seconds |
| Milliseconds | 500ms | Half a second |
| Minutes | 0:30 or 30s | 30 seconds |
| Full clock | 0:01:30 | 1 minute 30 seconds |
begin - when the animation starts
The begin attribute is surprisingly powerful. At its simplest, it's a time offset from document load:
<!-- Starts immediately -->
<animate ... begin="0s"/>
<!-- Starts 2 seconds after document load -->
<animate ... begin="2s"/>
<!-- Starts when parent element is clicked -->
<animate ... begin="click"/>
Click-to-start animation
Setting begin="click" means the animation waits until the user clicks the parent element (the one the <animate> is nested inside). No JavaScript needed:
<svg viewBox="0 0 400 120" xmlns="http://www.w3.org/2000/svg">
<circle cx="80" cy="60" r="30" fill="coral" style="cursor:pointer">
<animate attributeName="cx" from="80" to="320" dur="1s"
begin="click" fill="freeze"/>
</circle>
<text x="200" y="110" text-anchor="middle"
font-size="12">Click the circle</text>
</svg>
Click the coral circle - it slides to the right. No JavaScript, no event listeners. begin="click" is pure SMIL.
Chaining animations with event offsets
You can offset from any event trigger. begin="click+1.5s" means "1.5 seconds after the click." This lets you sequence multiple animations from a single user action:
<svg viewBox="0 0 400 120" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="60" r="20" fill="coral" style="cursor:pointer">
<!-- Step 1: move right (starts on click) -->
<animate attributeName="cx" from="50" to="350" dur="1.5s"
begin="click" fill="freeze"/>
<!-- Step 2: shrink 1.5s after click (= when move finishes) -->
<animate attributeName="r" from="20" to="8"
dur="0.5s" begin="click+1.5s" fill="freeze"/>
</circle>
</svg>
Click the coral circle. It moves right, then shrinks - the second animation uses begin="click+1.5s" to start exactly when the first finishes.
Syncbase timing (preview)
The spec also defines syncbase timing - referencing another animation by id:
<!-- Syncbase syntax (Lesson 10 will cover this in depth) -->
<animate id="anim1" attributeName="cx" from="50" to="300"
dur="2s" fill="freeze"/>
<!-- Starts when anim1 ends -->
<animate ... begin="anim1.end"/>
<!-- Starts 0.5s AFTER anim1 ends -->
<animate ... begin="anim1.end+0.5s"/>
The spec defines begin="animId.end" for chaining, but browser support is inconsistent - even Chrome can fail to fire syncbase-referenced animations on nested sibling <animate> elements targeting the same parent. The reliable cross-browser alternative is the event offset pattern shown above (begin="click+1.5s"). We'll explore syncbase workarounds and more robust sequencing patterns in Lesson 10.
repeatCount - how many cycles
repeatCount="3"- plays 3 times then stopsrepeatCount="indefinite"- loops foreverrepeatDur="10s"- keeps repeating until 10s total have elapsed (even mid-cycle)
4. The fill Attribute (Animation)
When a non-repeating animation ends, what should happen to the attribute?
| Value | Behaviour | Use case |
|---|---|---|
remove | Attribute snaps back to its base value (default) | Temporary effects, attention pulses |
freeze | Attribute holds its final animated value | One-shot transitions, reveals, state changes |
<svg viewBox="0 0 400 120" xmlns="http://www.w3.org/2000/svg">
<!-- fill="remove" (default) - snaps back -->
<circle cx="80" cy="60" r="30" fill="coral">
<animate attributeName="r" from="30" to="10" dur="2s"
fill="remove" repeatCount="1"/>
</circle>
<!-- fill="freeze" - stays at final value -->
<circle cx="250" cy="60" r="30" fill="#61afef">
<animate attributeName="r" from="30" to="10" dur="2s"
fill="freeze" repeatCount="1"/>
</circle>
</svg>
Both shrink from r=30 to r=10 over 2s. Left snaps back to 30 after; right stays at 10 permanently.
5. Interpolation: calcMode
The calcMode attribute controls how the browser interpolates between values. This is SMIL's easing system.
| Value | Behaviour | CSS equivalent |
|---|---|---|
linear | Constant speed between each pair of values (default for most attributes) | linear |
discrete | Jumps between values with no interpolation (default for non-numeric attributes like fill colour changes) | steps(1) |
paced | Constant velocity across all segments (ignores keyTimes) | No direct equivalent |
spline | Cubic Bézier easing between segments (requires keySplines) | cubic-bezier(...) |
calcMode="paced" ensures constant velocity regardless of the spacing of your keyframe values. If your values are "0;100;300", a linear animation would spend equal time on each segment (fast on the long segment, slow on the short one). paced adjusts timing so the element moves at the same speed throughout. This is invaluable for smooth motion animations.
calcMode="spline" with keySplines
For custom easing curves, set calcMode="spline" and provide keySplines. This uses the same cubic Bézier model as CSS cubic-bezier().
Each spline is four numbers: x1 y1 x2 y2 - the two control points of a cubic Bézier on a unit square where:
- X axis = normalised time within the segment (0 = start, 1 = end)
- Y axis = interpolation progress through the value change (0 = start value, 1 = end value)
This is a timing function, not a spatial curve. Unlike the Bézier curves in path commands (which define physical shapes in x/y space), this Bézier maps time to progress. The slope of the curve at any point represents the instantaneous velocity - a steep section means fast movement, a flat section means slow. You're not defining velocity directly; you're shaping the time-to-progress relationship, and velocity is its derivative.
| Easing | keySplines value | Meaning |
|---|---|---|
| ease-in | 0.42 0 1 1 | Starts slow, ends at full speed |
| ease-out | 0 0 0.58 1 | Starts at full speed, decelerates |
| ease-in-out | 0.42 0 0.58 1 | Slow start and end, fast middle |
| ease (CSS default) | 0.25 0.1 0.25 1 | Gentle version of ease-in-out |
| overshoot | 0.6 -0.28 0.74 0.05 | Goes past the target then settles |
Semicolons separate segments. If values has N entries, there are N−1 segments, so you need N−1 splines. For values="A;B;C" (2 segments): keySplines="<A→B curve>; <B→C curve>".
<svg viewBox="0 0 400 120" xmlns="http://www.w3.org/2000/svg">
<!-- Linear (for comparison) -->
<circle cx="30" cy="40" r="15" fill="grey" opacity="0.4">
<animate attributeName="cx" values="30;370;30"
dur="3s" repeatCount="indefinite"/>
</circle>
<!-- Spline-eased -->
<circle cx="30" cy="80" r="15" fill="coral">
<animate attributeName="cx"
values="30;370;30"
dur="3s"
calcMode="spline"
keyTimes="0;0.5;1"
keySplines="0.42 0 1 1; 0 0 0.58 1"
repeatCount="indefinite"/>
</circle>
</svg>
Both travel the same distance in the same time. The spline-eased circle accelerates and decelerates naturally.
6. keyTimes - Controlling Timing
keyTimes maps each value in the values list to a point in normalised time (0 to 1). It lets you spend more time on some segments and less on others.
<!-- values has 4 entries, so keyTimes needs 4 entries -->
<animate attributeName="cx"
values="50;200;350;200"
keyTimes="0;0.2;0.5;1"
dur="4s"
repeatCount="indefinite"/>
<!--
0 → 0.2 (20% of 4s = 0.8s): cx goes 50 → 200 (fast)
0.2 → 0.5 (30% of 4s = 1.2s): cx goes 200 → 350 (medium)
0.5 → 1.0 (50% of 4s = 2.0s): cx goes 350 → 200 (slow)
-->
The number of keyTimes values must equal the number of values. The first must be 0, the last must be 1 (for linear/spline modes). For discrete mode, the last doesn't have to be 1. For paced mode, keyTimes is ignored entirely.
7. The <set> Element
<set> is a simplified animation that performs a discrete value change at a point in time. No interpolation, no duration needed - it just switches an attribute to a new value. Think of it as calcMode="discrete" with a single target value.
<circle cx="200" cy="60" r="30" fill="coral">
<!-- Change colour to blue after 2 seconds -->
<set attributeName="fill" to="#61afef" begin="2s" fill="freeze"/>
</circle>
<set> is ideal for visibility toggles, colour state changes, and triggering discrete property switches.
<set> fully supports the begin attribute with all the same values as <animate> - time offsets, click events, and syncbase timing all work. This makes it ideal for click-driven state changes:
<!-- Colour change on click -->
<circle cx="100" cy="60" r="30" fill="coral" style="cursor:pointer">
<set attributeName="fill" to="#61afef" begin="click" fill="freeze"/>
</circle>
<!-- Visibility toggle after a delay -->
<rect x="10" y="10" width="80" height="40" fill="coral">
<set attributeName="visibility" to="hidden" begin="3s" fill="freeze"/>
</rect>
Cross-element triggers
The spec defines begin="elementId.click" for triggering an animation from a click on a different element. However, browser support for cross-element event references is unreliable (Chrome often ignores them). The workaround is to place the <set> inside the clickable element itself and target a different attribute, or use begin="click" directly on each element that should react. We'll explore cross-element orchestration patterns in Lesson 10.
For now, the reliable pattern is: <set> nested inside the element being clicked, controlling that same element:
<svg viewBox="0 0 300 100" xmlns="http://www.w3.org/2000/svg">
<!-- Click to change colour (set is inside the clickable element) -->
<rect x="50" y="20" width="200" height="60" rx="8"
fill="coral" style="cursor:pointer">
<set attributeName="fill" to="#61afef" begin="click" fill="freeze"/>
</rect>
<text x="150" y="57" text-anchor="middle"
font-size="16" fill="white" pointer-events="none">Click me</text>
</svg>
Click the rectangle - it turns blue permanently. <set> with begin="click" and fill="freeze" makes a one-shot state change.
Common uses for <set>:
- Toggling
visibilityordisplayat a specific time or on click - Changing a
hrefto swap images - Switching class names for CSS-driven state changes
- Any non-interpolatable attribute (strings, enumerations)
- Button-driven state machines - combine multiple
<set>elements to build toggle-style UI entirely in SVG
8. Animating Colour
Colour attributes (fill, stroke, stop-color) can be smoothly interpolated with <animate>. The browser handles the colour space interpolation:
<circle cx="200" cy="60" r="40" fill="coral">
<animate attributeName="fill"
values="coral;#61afef;#98c379;#e5c07b;coral"
dur="6s" repeatCount="indefinite"/>
</circle>
Smooth colour cycling through four colours. The browser interpolates between hex values in sRGB space.
9. Additive and Accumulative Animation
Two lesser-known but powerful attributes control how animated values combine:
| Attribute | Values | Effect |
|---|---|---|
additive | replace (default) / sum | sum: animated value is added to the base value instead of replacing it |
accumulate | none (default) / sum | sum: each repeat cycle starts from where the last one ended (values accumulate) |
additive and accumulate answer different questions:
additive→ "What do I do with the base attribute value within each cycle?" (replace: ignore it;sum: add to it)accumulate→ "What do I do between repeat cycles?" (none: reset;sum: build on previous)
Think of it this way: additive controls the spatial reference (relative to where?) and accumulate controls the temporal reference (does each repeat reset or build?).
Concrete example - <rect x="100"> with from="0" to="50", repeatCount="3":
| Settings | Cycle 1 | Cycle 2 | Cycle 3 |
|---|---|---|---|
| Neither (defaults) | x: 0→50 | x: 0→50 | x: 0→50 |
additive="sum" | x: 100→150 | x: 100→150 | x: 100→150 |
accumulate="sum" | x: 0→50 | x: 50→100 | x: 100→150 |
Both sum | x: 100→150 | x: 150→200 | x: 200→250 |
accumulate example
<!-- Without accumulate: bounces between 0 and 100 each cycle -->
<rect x="0" y="10" width="30" height="30" fill="coral">
<animate attributeName="x" from="0" to="100" dur="2s"
repeatCount="3" fill="freeze"/>
</rect>
<!-- With accumulate="sum": each cycle adds 100 more -->
<rect x="0" y="50" width="30" height="30" fill="#61afef">
<animate attributeName="x" from="0" to="100" dur="2s"
repeatCount="3" fill="freeze"
accumulate="sum"/>
</rect>
Top resets to 0 each cycle; bottom starts each cycle where the last ended (0→100, 100→200, 200→300).
additive example
<!-- additive="replace" (default): animated value IS the x value -->
<rect x="100" y="10" width="30" height="30" fill="coral">
<animate attributeName="x" from="0" to="50" dur="2s"
repeatCount="indefinite"/>
<!-- x goes 0→50, ignoring the base x="100" -->
</rect>
<!-- additive="sum": animated value is ADDED to base x="100" -->
<rect x="100" y="60" width="30" height="30" fill="#61afef">
<animate attributeName="x" from="0" to="50" dur="2s"
additive="sum" repeatCount="indefinite"/>
<!-- x goes (100+0)→(100+50) = 100→150 -->
</rect>
Both animate from="0" to="50". With replace, the rect jumps to x=0 then goes to 50 (ignoring base). With sum, the rect starts at its base (100) and shifts +0 to +50 from there.
10. Multiple Animations on One Element
You can stack multiple <animate> children on the same element, each targeting a different attribute. They run independently and simultaneously:
<circle cx="200" cy="60" r="20" fill="coral">
<!-- Animate position -->
<animate attributeName="cx" values="50;350;50"
dur="4s" repeatCount="indefinite"/>
<!-- Animate size simultaneously -->
<animate attributeName="r" values="20;35;20"
dur="2s" repeatCount="indefinite"/>
<!-- Animate opacity simultaneously -->
<animate attributeName="opacity" values="1;0.4;1"
dur="4s" repeatCount="indefinite"/>
</circle>
Three independent animations on one circle: position (4s cycle), radius (2s cycle), opacity (4s cycle). Different durations create complex-looking motion from simple rules.
Using different dur values on multiple animations of the same element creates polyrhythmic motion - the overall pattern doesn't repeat until the least common multiple of all durations. A 3s and 4s animation won't sync up until 12s. This is a cheap way to create organic, non-repetitive-looking animation from very simple declarations.
11. SMIL and the CSS Cascade
You might worry: if CSS overrides presentation attributes (Lesson 4), and SMIL animates attributes, won't CSS block SMIL? No - SMIL operates at a higher priority than both.
The actual priority order (highest wins):
- SMIL animation (special "override" layer - always wins while active)
- CSS
!important - CSS specificity (stylesheets,
<style>blocks) - Presentation attributes (
fill="coral") - Inherited values
<style>
.styled { fill: blue; } /* CSS wins over presentation attr */
</style>
<circle class="styled" fill="red" cx="100" cy="60" r="30">
<!-- SMIL wins over CSS - circle animates to green -->
<animate attributeName="fill" to="green" dur="2s"
fill="freeze"/>
</circle>
<!-- Priority: SMIL (green) > CSS (blue) > attribute (red) -->
The SVG spec defines SMIL animations as applying after the CSS cascade resolves - they operate in a special override step. This means you can safely use CSS for static styling and SMIL for animation on the same element. When a fill="remove" animation ends, CSS takes back control. When fill="freeze", SMIL holds indefinitely.
The CSS/SMIL conflict only arises for properties that exist in both worlds (like fill, stroke, opacity). Geometry attributes like cx, cy, r, d, viewBox are pure SVG attributes with no CSS equivalent - CSS can't set them, so there's never a conflict. SMIL animates them freely.
12. What Can (and Can't) Be Animated
SMIL can animate any presentation attribute or geometry attribute that takes a numeric or colour value. Some examples:
| Animatable | Examples |
|---|---|
| Geometry | x, y, cx, cy, r, rx, ry, width, height, points, d (path data!) |
| Paint | fill, stroke, stop-color, flood-color |
| Opacity | opacity, fill-opacity, stroke-opacity |
| Stroke | stroke-width, stroke-dasharray, stroke-dashoffset |
| Transforms | via <animateTransform> (Lesson 10) |
| Filter params | stdDeviation, dx/dy on filter prims, etc. |
| Text | startOffset, textLength, dx, dy |
| viewBox | Yes! Animating viewBox creates zoom/pan effects |
| Not directly animatable with SMIL | Workaround |
|---|---|
class attribute | Use <set> to swap class names |
| CSS custom properties | Animate the underlying attribute instead |
transform (as a string) | Use <animateTransform> |
clip-path / mask references | Animate the shapes inside the clip/mask |
You can animate the d attribute of a <path> to morph between shapes. The constraint: both path data strings must have the same number and type of commands. You can't morph a path with 3 segments into one with 7 - the browser needs a 1:1 mapping between points. The points attribute on <polygon>/<polyline> is simpler - just match the number of coordinate pairs (no command types to worry about). Both are the basis of shape morphing animations (Lesson 13 capstone material).
13. Practical Example: Breathing Circle
Combining the techniques covered so far into a polished, production-quality animation:
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="40" fill="none"
stroke="coral" stroke-width="3" opacity="0.8">
<!-- Radius breathes in and out -->
<animate attributeName="r" values="40;60;40"
dur="4s" calcMode="spline"
keyTimes="0;0.5;1"
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1"
repeatCount="indefinite"/>
<!-- Opacity pulses gently -->
<animate attributeName="opacity" values="0.8;0.3;0.8"
dur="4s" calcMode="spline"
keyTimes="0;0.5;1"
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1"
repeatCount="indefinite"/>
<!-- Stroke thins as it expands -->
<animate attributeName="stroke-width" values="3;1;3"
dur="4s" repeatCount="indefinite"/>
</circle>
</svg>
A "breathing" circle: radius, opacity, and stroke-width all animate with eased timing. Feels organic because of the spline easing - compare to a linear version and the difference is dramatic.
14. Practical Example: Loading Spinner
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" fill="none"
stroke="#61afef" stroke-width="6"
stroke-dasharray="200" stroke-dashoffset="200"
stroke-linecap="round">
<!-- Draw the stroke -->
<animate attributeName="stroke-dashoffset"
values="200;0;200" dur="2s"
repeatCount="indefinite"/>
</circle>
<!-- Rotate the whole circle for spin -->
<animateTransform attributeName="transform" type="rotate"
values="0 50 50;360 50 50" dur="1.5s"
repeatCount="indefinite"
xlink:href="circle"/>
</svg>
A loading spinner combining stroke-dashoffset animation (the drawing effect) with rotation. Two animations, zero JavaScript. This uses <animateTransform> which we'll cover fully in Lesson 10.
15. Browser Support & the "Deprecated" Myth
You may have read that SMIL is "deprecated." Here's the actual situation:
- Chrome announced deprecation intent in 2015, then reversed it in 2016. SMIL remains fully supported.
- Firefox - full support, never threatened removal.
- Safari - full support.
- Edge (Chromium) - full support (same engine as Chrome).
- IE - never supported SMIL. IE is dead, so this doesn't matter.
SMIL is not deprecated in any current browser. The SVG 1.1 spec defines it, and the SVG Animations Level 2 spec is actively developed. For self-contained SVG animations, SMIL remains the best tool.
Respect prefers-reduced-motion. While SMIL doesn't directly respond to this media query (unlike CSS animations), you can use a <style> block to hide or pause animated elements: @media (prefers-reduced-motion: reduce) { animate, set, animateTransform, animateMotion { display: none; } }
16. SMIL vs CSS vs JavaScript - When to Use What
The browser has three completely independent animation systems. They're not wrappers around each other - each has its own spec, its own engine, and its own strengths:
| SMIL | CSS Animations | JavaScript | |
|---|---|---|---|
| Lives in | SVG markup (XML elements) | Stylesheets / <style> | <script> blocks |
| Targets | Any SVG attribute (geometry, paint, filters, path data, viewBox) | CSS properties only | Anything via DOM |
| Self-contained | Yes - no dependencies | Needs a <style> block | Needs a <script> block |
| Orchestration | Native syncbase timing (anim1.end+0.5s) | Manual delay chaining | Full programmatic control |
| Easing | Linear, discrete, paced, spline | Linear, ease, cubic-bezier, steps | You calculate it |
| Performance | CPU (paint step) | GPU compositor (for opacity/transform) | Depends on implementation |
| Path morphing | Native (d attribute) | Limited (some browsers support via CSS d) | Full control with libraries |
| Event triggers | Built-in (begin="click") | Requires JS to toggle classes | Native |
Decision guide
| Use case | Best tool | Why |
|---|---|---|
| Self-contained SVG file (no external deps) | SMIL | No <style> or <script> needed - works in <img> tags |
| Hover/focus effects on inline SVG | CSS | CSS transitions are simpler and GPU-accelerated for opacity/transform |
Animate d (path morphing) | SMIL | Native support; CSS d property support is inconsistent |
Animate viewBox (zoom/pan) | SMIL | Only SMIL can animate this declaratively |
| Complex choreography with user interaction | JavaScript | Conditional logic, state machines, physics |
| Performance-critical (60fps transform/opacity) | CSS | GPU compositor layer - doesn't repaint |
| Sequenced animation chain | SMIL | Syncbase timing makes this trivial declaratively |
| Data-driven or generative animation | JavaScript | Need loops, conditionals, API data |
SVG inside <img> tag | SMIL | Only option - <img> blocks scripts and external CSS |
You can (and often will) mix them. Use SMIL for the base animation loop, CSS for hover states and transitions, and JavaScript for user-driven interactivity. They compose well because they operate on different layers of the rendering pipeline.
Performance differences
The key performance distinction is where the animation runs in the rendering pipeline:
| Approach | Where it runs | Cost per frame |
|---|---|---|
CSS transform / opacity | GPU compositor thread | Nearly free - no repaint |
CSS other properties (fill, stroke, etc.) | CPU repaint | Moderate |
| SMIL (any attribute) | CPU repaint | Moderate |
SMIL on filter params (stdDeviation, etc.) | CPU repaint + re-rasterise filter | Expensive |
| JavaScript + rAF manipulating DOM | CPU + potential layout thrash | Varies widely |
CSS transform and opacity are special - the browser promotes the element to its own compositor layer (a GPU texture) and animates it without touching the main thread. No repaint, no relayout. That's why they hit smooth 60fps even on a busy page. Everything else - including SMIL and CSS animating fill/stroke - triggers a CPU repaint each frame.
For the kind of self-contained SVG animations you're building (5–15 elements, moderate complexity), SMIL performance is not a concern. It becomes relevant with 30+ simultaneous animations, high-frequency cycling, animated filters (which re-run the entire filter pipeline every frame), or mobile devices with weak CPUs. For anything that maps to transform or opacity, prefer CSS for the GPU fast-path. Use SMIL for everything else.
Quiz
Question 1
What does fill="freeze" on an <animate> element do?
Question 2
What is the difference between calcMode="linear" and calcMode="paced" when animating through the values "0;100;300"?
Question 3
How do you make a SMIL animation start when the user clicks the animated element?
trigger="onclick" on the animate elementbegin="click" on the animate element<event type="click"> elementQuestion 4
What constraint must be met to animate the d attribute (path morphing)?
Question 5
What does accumulate="sum" do on a repeating animation?
Exercises
- Pulsing dot - Create a circle that pulses its radius and opacity in a breathing pattern using
calcMode="spline"for natural easing. Make it feel alive, not mechanical. - Colour cycle - Animate a rectangle's fill through 5 colours of your choice over 10 seconds, looping indefinitely.
- Progress bar - A horizontal rect whose width animates from 0 to full width over 3 seconds, then freezes. Use
fill="freeze". - Bouncing ball - Animate a circle's
cywithvaluesandkeySplinesto create a realistic bounce (fast at bottom, slow at top). Hint: use ease-in on the way down and ease-out on the way up. - Polyrhythm - Put 3 animations on one element with durations 3s, 4s, and 5s. Observe how long it takes before the pattern repeats exactly.