What is SMIL? SMIL stands for Synchronized Multimedia Integration Language. It is the W3C standard baked directly into SVG that lets you animate shapes, paths, transforms, colours, and more — zero JavaScript, zero CSS, zero external libraries required. The browser does all the work natively inside the SVG element.
In this guide we will build every animation you see in the roomie wordmark below step by step, explaining why each attribute exists and how to tweak it for your own projects.
┌─────────────────────────────────────────┐
│ r [💬💬] m i e │
│ ───────── │
└─────────────────────────────────────────┘
Two chat bubbles slide in from below and fade up into position when the SVG loads. The entire effect is ~30 lines of markup.
Table of Contents
- The SVG canvas and viewport
- Defining reusable assets with
<defs> - Painting with gradients
- Drawing the chat bubble paths
- Your first SMIL animation —
<animate>for opacity - Moving things —
<animateTransform> - Controlling timing —
keyTimesandbegin - Natural easing —
calcModeandkeySplines - Locking the final state —
fill="freeze" - Adding atmosphere — glow ellipse and accent line
- The finished file
- Common tasks and how to tackle them
- Browser support and gotchas
1. The SVG canvas and viewport
Every SVG starts with its coordinate system. The viewBox attribute maps internal coordinates to the rendered size.
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 420 160"
width="420"
height="160"
role="img"
aria-label="roomie"
>
<title>roomie</title>
| Attribute | Why it matters |
|---|---|
viewBox="0 0 420 160" |
Internal grid: 420 units wide, 160 units tall. All coordinates below live in this space. |
width / height
|
Rendered pixel size. Change these to scale the whole logo without touching a single coordinate. |
role="img" + aria-label
|
Screen-reader accessibility. Always include these on decorative/logo SVGs. |
Task tip: Always design at a comfortable internal resolution (here 420 × 160) and rely on viewBox scaling rather than hard-coding pixel sizes everywhere.
2. Defining reusable assets with <defs>
<defs> is a container for things that are defined but not rendered until referenced via url(#id). Think of it as your SVG stylesheet.
<defs>
<!-- gradients go here -->
</defs>
Nothing inside <defs> paints pixels on its own. It only activates when another element references it.
3. Painting with gradients
The wordmark uses three gradient types. Here is each one explained.
3a. Radial gradient — the ambient glow
<radialGradient id="glow-b" cx="0.5" cy="0.5" r="0.55">
<stop offset="0" stop-color="#3b82f6" stop-opacity="0.25"/>
<stop offset="1" stop-color="#3b82f6" stop-opacity="0"/>
</radialGradient>
cx, cy, r use the default gradientUnits="objectBoundingBox", so 0.5 means centre of the element that references this gradient. The gradient fades from 25% opacity blue in the centre to fully transparent at the edge — a classic "light bloom" trick.
3b. Linear gradient — left bubble (indigo → slate)
<linearGradient id="bubbleGradA-b" x1="0.1" y1="0.1" x2="0.9" y2="0.9">
<stop offset="0" stop-color="#4f46e5"/>
<stop offset="0.5" stop-color="#3730a3"/>
<stop offset="1" stop-color="#1e1b4b"/>
</linearGradient>
x1/y1 → x2/y2 defines the gradient direction as a diagonal line (top-left to bottom-right). Three stops give depth: a lighter highlight at the top-left, mid-tone in the centre, and a near-black shadow at the bottom-right.
3c. Linear gradient — right bubble (cyan → blue)
<linearGradient id="bubbleGradB-b" x1="0.9" y1="0.1" x2="0.1" y2="0.9">
<stop offset="0" stop-color="#06b6d4"/>
<stop offset="0.55" stop-color="#2563eb"/>
<stop offset="1" stop-color="#4f46e5"/>
</linearGradient>
The direction is mirrored (x1="0.9" starts at the right): cyan hits the top-right edge, transitioning into the same indigo as the left bubble — they visually blend where they overlap.
3d. Linear gradient — text
<linearGradient id="textGrad-b" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#e0e7ff"/>
<stop offset="0.4" stop-color="#ffffff"/>
<stop offset="0.7" stop-color="#a5f3fc"/>
<stop offset="1" stop-color="#c7d2fe"/>
</linearGradient>
x1="0" x2="1" with identical y values = pure horizontal sweep. The stops produce a subtle shimmer: near-white in the middle with cool lavender on the left and cyan on the right.
Apply a gradient by setting fill="url(#gradient-id)" on any shape or text element.
4. Drawing the chat bubble paths
Both bubbles use the SVG <path> element with Cubic Bézier curves (C command).
<path
d="M62 14
C39 14 21 30 21 51
C21 60 24.8 68 31 74
C30.4 81.5 27.5 87.5 22 92.5
C31 91.8 38.5 89.2 45 85
C50 86.6 55.8 87.5 62 87.5
C85 87.5 103 71.5 103 51
C103 30 85 14 62 14Z"
fill="url(#bubbleGradA-b)"
stroke="rgba(129,140,248,0.20)"
stroke-width="1.2"
stroke-linejoin="round"
/>
Path anatomy:
| Command | Meaning |
|---|---|
M62 14 |
Move to the starting point (top-centre of bubble) |
C x1 y1, x2 y2, x y |
Cubic Bézier curve: two control points then the end point |
Z |
Close the path back to the starting point |
The "tail" at the bottom-left (points around 22 92.5) is drawn as a separate Bézier arc, giving the classic chat-bubble tail shape.
Subtle inner shine:
<ellipse cx="55" cy="38" rx="22" ry="10" fill="white" opacity="0.06"/>
A white ellipse at 6% opacity in the top-left of the bubble mimics light catching a glossy surface.
5. Your first SMIL animation — <animate> for opacity
The <animate> element changes a single numeric attribute of its parent over time.
<animate
attributeName="opacity"
values="0; 1"
keyTimes="0; 1"
dur="0.35s"
begin="0s"
fill="freeze"
repeatCount="1"
/>
Place it as a direct child of the element you want to animate — the <g> that wraps the bubble path.
| Attribute | Value | What it does |
|---|---|---|
attributeName |
opacity |
Which SVG attribute to animate |
values |
0; 1 |
Start at invisible, end at fully opaque |
keyTimes |
0; 1 |
Map each value to a position in the timeline (0 = start, 1 = end) |
dur |
0.35s |
Total duration of this animation |
begin |
0s |
Fire immediately when the SVG is parsed |
fill |
freeze |
Keep the final value after the animation ends (explained in §9) |
repeatCount |
1 |
Play once |
Three-stop keyframe example: if you wanted a pulse then fade you'd write:
<animate
attributeName="opacity"
values="0; 1; 0.5; 1"
keyTimes="0; 0.3; 0.7; 1"
dur="1s"
fill="freeze"
repeatCount="1"
/>
6. Moving things — <animateTransform>
While <animate> targets scalar attributes, <animateTransform> handles CSS-transform-style operations: translate, rotate, scale, skewX, skewY.
The left bubble slides up from below:
<animateTransform
attributeName="transform"
type="translate"
values="0 18; 0 -5; 0 0"
keyTimes="0; 0.65; 1"
calcMode="spline"
keySplines="0.22 1 0.36 1; 0.5 0 0.5 1"
dur="0.7s"
begin="0s"
fill="freeze"
repeatCount="1"
/>
Breaking down values for translate
values for type="translate" uses "x y" pairs separated by semicolons:
| Keyframe | Value | Meaning |
|---|---|---|
| 0 | 0 18 |
Start 18 units below final position |
| 0.65 | 0 -5 |
Overshoot 5 units above final position (bounce) |
| 1 | 0 0 |
Settle at the natural resting position |
The three-point keyframe with an overshoot is the foundation of spring animation. The bubble shoots past its target and gently falls back, feeling physical rather than mechanical.
Right bubble — slightly different timing:
values="-8 21; -8 -3; -8 3"
The x component stays at -8 throughout (its group already has transform="translate(-8, 3)"). The y goes from 21 → -3 → 3, a slightly shorter overshoot so the two bubbles settle with a staggered rhythm.
7. Controlling timing — keyTimes and begin
keyTimes="0; 0.65; 1"
keyTimes is a semicolon-separated list that maps each entry in values to a point in the animation timeline, expressed as a fraction from 0 (start) to 1 (end).
Timeline: |──────────────────────────────────|
0 1
keyTimes: 0 0.65 1
values: "0 18" "0 -5" "0 0"
This means:
- 65% of the time is spent on the main upward slide
- 35% of the time is spent on the overshoot correction
Staggering animations with begin:
<!-- First element fires at 0s -->
<animateTransform ... dur="0.7s" begin="0s" />
<!-- A second element fires 150ms later -->
<animateTransform ... dur="0.7s" begin="0.15s" />
You can also synchronise to another element's lifecycle:
<!-- Fires when "bubble-a" ends -->
<animateTransform ... begin="bubble-a.end" />
8. Natural easing — calcMode and keySplines
By default SMIL interpolates linearly (calcMode="linear"). For organic motion, switch to calcMode="spline" and provide cubic Bézier curves via keySplines.
calcMode="spline"
keySplines="0.22 1 0.36 1; 0.5 0 0.5 1"
Each keySplines entry corresponds to a segment between consecutive keyframes. For three keyframes you have two segments, hence two spline definitions separated by ;.
Reading a cubic Bézier: x1 y1 x2 y2
"0.22 1 0.36 1" → ease-out (slow finish)
"0.5 0 0.5 1" → ease-in-out (slow start AND slow finish)
You can visualise these at cubic-bezier.com. The equivalent CSS equivalents are:
| SMIL keySpline | CSS equivalent |
|---|---|
0 0 1 1 |
linear |
0.25 0.1 0.25 1 |
ease |
0 0 0.2 1 |
ease-out |
0.4 0 1 1 |
ease-in |
0.22 1 0.36 1 |
custom spring-like ease-out |
Quick cheat sheet for spring animation:
- Segment 1 (travel to overshoot):
0.22 1 0.36 1— fast exit, decelerates hard - Segment 2 (correction):
0.5 0 0.5 1— symmetrical ease so the settle feels balanced
9. Locking the final state — fill="freeze"
After an animation ends, the default behaviour (fill="remove") snaps the element back to its pre-animation value. That means your bubble would teleport back to y=18 and disappear.
fill="freeze"
fill="freeze" tells the animation engine to hold the final keyframe value indefinitely. Always use this for entrance animations that you want to stay visible.
10. Adding atmosphere — glow ellipse and accent line
Ambient glow
<ellipse cx="130" cy="78" rx="100" ry="55" fill="url(#glow-b)"/>
This large translucent ellipse uses the radial gradient from §3a. Because the radial gradient fades to stop-opacity="0" at its edge, the ellipse blends seamlessly into the background — no hard border.
Accent line
<line
x1="14" y1="134" x2="130" y2="134"
stroke="url(#bubbleGradB-b)"
stroke-width="2"
stroke-linecap="round"
opacity="0.35"
/>
stroke-linecap="round" caps both ends of the line with half-circles, eliminating the harsh square nub the default produces. The line reuses bubbleGradB-b so the cyan-to-indigo sweep echoes the right bubble above it.
11. The finished file
Here is the complete roomie-blue.svg assembled from all the steps above:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 420 160" width="420" height="160"
role="img" aria-label="roomie">
<title>roomie</title>
<defs>
<desc>Roomie wordmark with chat bubbles.</desc>
<!-- Ambient glow -->
<radialGradient id="glow-b" cx="0.5" cy="0.5" r="0.55">
<stop offset="0" stop-color="#3b82f6" stop-opacity="0.25"/>
<stop offset="1" stop-color="#3b82f6" stop-opacity="0"/>
</radialGradient>
<!-- Left bubble: deep indigo → slate blue -->
<linearGradient id="bubbleGradA-b" x1="0.1" y1="0.1" x2="0.9" y2="0.9">
<stop offset="0" stop-color="#4f46e5"/>
<stop offset="0.5" stop-color="#3730a3"/>
<stop offset="1" stop-color="#1e1b4b"/>
</linearGradient>
<!-- Right bubble: electric blue → cyan -->
<linearGradient id="bubbleGradB-b" x1="0.9" y1="0.1" x2="0.1" y2="0.9">
<stop offset="0" stop-color="#06b6d4"/>
<stop offset="0.55" stop-color="#2563eb"/>
<stop offset="1" stop-color="#4f46e5"/>
</linearGradient>
<!-- Text shimmer -->
<linearGradient id="textGrad-b" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#e0e7ff"/>
<stop offset="0.4" stop-color="#ffffff"/>
<stop offset="0.7" stop-color="#a5f3fc"/>
<stop offset="1" stop-color="#c7d2fe"/>
</linearGradient>
</defs>
<!-- Ambient glow bloom -->
<ellipse cx="130" cy="78" rx="100" ry="55" fill="url(#glow-b)"/>
<!-- "r" wordmark -->
<text x="8" y="120"
fill="url(#textGrad-b)"
font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"
font-weight="800" font-size="118" letter-spacing="-4">r</text>
<!-- Chat bubbles group -->
<g transform="translate(28, 26)">
<!-- Left bubble -->
<g>
<animateTransform attributeName="transform" type="translate"
values="0 18; 0 -5; 0 0" keyTimes="0; 0.65; 1"
calcMode="spline" keySplines="0.22 1 0.36 1; 0.5 0 0.5 1"
dur="0.7s" begin="0s" fill="freeze" repeatCount="1"/>
<animate attributeName="opacity" values="0; 1"
keyTimes="0; 1" dur="0.35s" begin="0s" fill="freeze" repeatCount="1"/>
<path
d="M62 14 C39 14 21 30 21 51 C21 60 24.8 68 31 74
C30.4 81.5 27.5 87.5 22 92.5 C31 91.8 38.5 89.2 45 85
C50 86.6 55.8 87.5 62 87.5 C85 87.5 103 71.5 103 51
C103 30 85 14 62 14Z"
fill="url(#bubbleGradA-b)"
stroke="rgba(129,140,248,0.20)" stroke-width="1.2" stroke-linejoin="round"/>
<ellipse cx="55" cy="38" rx="22" ry="10" fill="white" opacity="0.06"/>
</g>
<!-- Right bubble -->
<g transform="translate(-8, 3)">
<animateTransform attributeName="transform" type="translate"
values="-8 21; -8 -3; -8 3" keyTimes="0; 0.7; 1"
calcMode="spline" keySplines="0.22 1 0.36 1; 0.5 0 0.5 1"
dur="0.85s" begin="0s" fill="freeze" repeatCount="1"/>
<animate attributeName="opacity" values="0; 1"
keyTimes="0; 1" dur="0.35s" begin="0s" fill="freeze" repeatCount="1"/>
<path
d="M132 17 C112 17 96 31.5 96 50 C96 58 99 65 104.5 70.5
C104 77.5 101.5 83 97 87.5 C104.5 87 111.5 84.8 117.5 81
C121.5 82.3 126 83 131 83 C151 83 167 68.5 167 50
C167 31.5 151 17 132 17Z"
fill="url(#bubbleGradB-b)"
stroke="rgba(34,211,238,0.15)" stroke-width="1.2" stroke-linejoin="round"/>
<ellipse cx="138" cy="37" rx="18" ry="8" fill="white" opacity="0.07"/>
</g>
<!-- Connection spark -->
<circle cx="96" cy="50" r="2.5" fill="#67e8f9" opacity="0.5"/>
<circle cx="96" cy="50" r="5" fill="#67e8f9" opacity="0.12"/>
</g>
<!-- "mie" wordmark -->
<text x="178" y="120"
fill="url(#textGrad-b)"
font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"
font-weight="800" font-size="118" letter-spacing="-4">mie</text>
<!-- Accent underline -->
<line x1="14" y1="134" x2="130" y2="134"
stroke="url(#bubbleGradB-b)" stroke-width="2"
stroke-linecap="round" opacity="0.35"/>
</svg>
12. Common tasks and how to tackle them
Task: Make the animation loop forever
<!-- Change repeatCount="1" → "indefinite" on both animations -->
<animateTransform ... repeatCount="indefinite"/>
<animate ... repeatCount="indefinite"/>
For a looping entrance you'll also want fill="remove" (the default) so each iteration resets to the start value.
Task: Trigger the animation on hover (CSS + SMIL hybrid)
SMIL begin accepts event-based triggers:
<animateTransform
...
begin="mouseover"
end="mouseout"
fill="freeze"
repeatCount="1"
/>
For <img src="logo.svg"> embeds this won't fire (the SVG is sandboxed). Use the SVG inline in HTML or via <object> for event-based triggers.
Task: Add a colour-cycling animation
<animate
attributeName="stop-color"
values="#4f46e5; #06b6d4; #4f46e5"
keyTimes="0; 0.5; 1"
dur="4s"
repeatCount="indefinite"
calcMode="spline"
keySplines="0.4 0 0.6 1; 0.4 0 0.6 1"
/>
Place this directly inside a <stop> element inside a gradient. Note: attributeName="stop-color" uses the hyphenated CSS property name, not a presentation attribute.
Task: Rotate an element around its own centre
<animateTransform type="rotate"> values format: "angle cx cy".
<!-- Spin the connection spark 360° around its own centre -->
<animateTransform
attributeName="transform"
type="rotate"
values="0 96 50; 360 96 50"
dur="3s"
repeatCount="indefinite"
/>
96 50 are the cx/cy of the circle — the pivot point.
Task: Scale an element for a "pulse" badge effect
<animateTransform
attributeName="transform"
type="scale"
values="1; 1.15; 1"
keyTimes="0; 0.5; 1"
dur="1.4s"
repeatCount="indefinite"
calcMode="spline"
keySplines="0.4 0 0.6 1; 0.4 0 0.6 1"
additive="sum"
/>
additive="sum" is critical when the element already has a transform attribute — it adds to the existing transform instead of replacing it.
Task: Stagger multiple elements with begin offsets
<!-- Element 1: fires at 0s -->
<animate id="a1" ... begin="0s" dur="0.6s"/>
<!-- Element 2: fires after element 1 ends -->
<animate ... begin="a1.end" dur="0.6s"/>
<!-- Element 3: fires 0.2s after element 1 ends -->
<animate ... begin="a1.end+0.2s" dur="0.6s"/>
The roomie logo uses a simpler form — both bubbles start at begin="0s" but have different dur values (0.7s vs 0.85s), creating a natural stagger through speed difference rather than delayed starts.
Task: Debug an animation that isn't playing
-
Check
repeatCount="1"+fill="freeze"— withoutfill="freeze"the element returns to its original state and looks broken. -
Confirm the SVG is inline or in
<object>—<img>tags block SMIL. -
Inspect
keyTimescount — must equal the count of semicolon-delimited values invalues. -
Inspect
keySplinescount — must equalkeyTimes count - 1. -
Firefox quirk —
attributeName="transform"must be lowercase; Firefox is case-sensitive.
13. Browser support and gotchas
| Browser | SMIL support | Notes |
|---|---|---|
| Chrome / Edge | ✅ Full | Chromium has had full SMIL support since 2013 |
| Firefox | ✅ Full | Case-sensitive attribute names |
| Safari | ✅ Full | iOS Safari too |
| IE 11 | ❌ None | IE never supported SMIL |
<img> embed |
⚠️ Partial | Declarative animations play; event-based triggers (mouseover) do not |
background-image CSS |
❌ None | SVG animations are blocked entirely |
Use SMIL when:
- You want self-contained SVG files (logos, icons, loaders)
- You need animations in email clients that support SVG
- You want zero runtime dependencies
Use CSS animations instead when:
- You need
prefers-reduced-motionmedia query support (SMIL ignores it by default — add JS to pause if needed) - You want DevTools animation inspector support
- You're animating SVGs that are part of a larger component tree
Wrapping up
SMIL gives you a remarkably complete animation toolkit inside a plain XML file. The roomie wordmark shows how much you can achieve in ~30 lines:
- ✅ Gradient fills (radial + linear)
- ✅ Smooth entrance animation with spring overshoot
- ✅ Opacity fade-in
- ✅ Natural cubic Bézier easing
- ✅ Staggered multi-element timing
- ✅ Atmospheric glow and subtle shines
The mental model is simple: put your <animate> or <animateTransform> inside the element it should affect, describe what changes in values, when in keyTimes, and how fast in dur. The rest is taste.
Have questions or want to share your own SMIL animation? Drop them in the comments below.
Top comments (0)