Lesson 39 • Advanced Track
SVG Mastery: Paths, Gradients, Animation
By the end of this lesson you'll be able to draw crisp, resolution-independent graphics in pure markup, style and theme them with CSS custom properties, animate a line so it appears to draw itself, build a reusable icon sprite, and make every graphic accessible to screen readers.
What You'll Learn
var() or @keyframes feel new, skim CSS Custom Properties first — this lesson reuses both.💡 Think of It Like This
A PNG is a photograph; an SVG is a recipe. A photo of a cake is a fixed grid of coloured dots — blow it up on a billboard and you see the dots. A recipe says "a 20cm round tin, two layers" — hand it to a baker and they can make the cake at any size, perfectly, every time.
SVG stores your graphic as a recipe in text: "draw a circle of radius 50 at (60, 60), fill it green." The browser is the baker. Because it's a recipe, not a snapshot, it stays sharp at any zoom, every ingredient is a real DOM element you can style and animate, and you can change one line to re-colour the whole thing.
1. Inline SVG & the viewBox Coordinate System
Inline SVG means writing the <svg> tag straight into your HTML instead of pointing an <img> at a file. The payoff is that every shape inside becomes a real DOM element — you can target it with CSS, hook up events, and inspect it in DevTools.
The single most important attribute is viewBox="minX minY width height". It sets up the SVG's own little coordinate grid: viewBox="0 0 100 100" means "inside here, the world is 100 units wide and tall." The width/height (or CSS) decide how big that grid is drawn on screen — so a 100-unit grid can be painted at 50px or 500px and the coordinates inside never change. Note (0, 0) is the top-left, and y grows downward.
Run this. The two squares are drawn with identical coordinates but different on-screen sizes — proof the viewBox is independent of pixels.
Worked example: one coordinate grid, two sizes
The viewBox decides the units; width/height decide the pixels
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>viewBox basics</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; }
svg { border: 1px dashed #94a3b8; } /* show each SVG's box */
</style>
</head>
<body>
<h2>Same drawing, two sizes</h2>
<!-- viewBox="0 0 100 100": the inside world is 100 x 100 units.
width/height = 80px: that world is PAINTED at 80 pixels. -->
<svg viewBox="0 0 100 100" width="80" height="80">
<rect x
...2. Shapes, fill & stroke
A handful of elements cover most graphics: <rect> (x, y, width, height, plus rx for rounded corners), <circle> (cx, cy centre and r radius), and the do-anything <path>, whose d attribute is a mini drawing language: M move to, L line to, Q/C curves, Z close.
Every shape has two paint properties: fill (the inside) and stroke (the outline, sized with stroke-width). Set fill="none" for an outline-only shape.
| Element | Key attributes | Use for |
|---|---|---|
<rect> | x, y, width, height, rx | Boxes, rounded cards |
<circle> | cx, cy, r | Dots, avatars, badges |
<path> | d (M, L, Q, C, Z) | Anything — icons, curves |
Worked example: build a checkmark badge icon
rect, circle and path combined, coloured with fill and stroke
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Shapes, fill & stroke</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; background:#0f172a; }
</style>
</head>
<body>
<!-- role="img" + <title> make this icon meaningful to screen readers (section 6). -->
<svg viewBox="0 0 64 64" width="160" height="160" role="img" aria-labelledby="ok-title">
<title id="ok-title">Success: task complete</title>
<!-- circle: cx/cy is the CENTRE, r
...3. Styling SVG with CSS & Custom Properties
Because each shape is a DOM element, the same CSS you already know works on it — just remember the properties are fill, stroke and stroke-width rather than background and border. A CSS rule beats a fill="..." attribute, so you can leave a sensible colour in the markup and override it for hover or dark mode.
The killer combo is custom properties: store the colour in a --var and read it with var() inside fill. Now one variable re-themes the icon, and stroke="currentColor" makes an icon inherit the surrounding text colour automatically.
Worked example: theme an icon with a custom property
fill: var(--icon) and stroke: currentColor driven entirely by CSS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Styling SVG with CSS</title>
<style>
:root { --icon: #3b82f6; } /* one token themes every icon below */
body { font-family: system-ui, sans-serif; padding: 24px; background:#0f172a; color:#e5e7eb; }
.row { display:flex; gap:24px; align-items:center; }
/* fill reads the variable; change --icon once and all hearts re-colour. */
.heart { fill: var(--icon); transition: fill .2s, transform .2s; cursor:
...4. Animating SVG with CSS (the draw effect)
The famous "line draws itself" effect is a trick with two stroke properties. stroke-dasharray turns a solid line into dashes; make the single dash as long as the whole path and you get one big dash. stroke-dashoffset then slides that dash along the line — push it fully off the end and the line looks empty. Animate the offset back to 0 and the line appears to draw itself.
It only affects the stroke, so the shape needs fill: none and a stroke. For the exact length use path.getTotalLength() in JS; here a value safely larger than the path works fine.
SMIL note: SVG also has its own animation tags — <animate> and <animateTransform>, called SMIL. Prefer CSS (shown here) for almost everything: it's better supported, easier to control from JavaScript, and lives with the rest of your styles. Reach for SMIL only for the rare attribute CSS can't animate.
Worked example: a self-drawing signature line
stroke-dasharray + stroke-dashoffset animated to 0
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SVG draw animation</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; background:#0f172a; }
/* 1) dasharray makes one dash 600 units long (longer than the path).
2) dashoffset 600 pushes that dash completely off-screen (line empty).
3) the animation slides the offset back to 0 -> the line "draws". */
.draw {
fill: none; /* draw effect works on the ST
...5. Reuse with <symbol> & <use>
Copy-pasting the same icon markup ten times is wasteful and a nightmare to update. Instead, define each icon once inside a <symbol id="..."> (give the symbol its own viewBox), then stamp it anywhere with <use href="#id" />. This is an "SVG sprite" — one definition, many instances.
Each <use> can be a different size and colour, and because the shapes use currentColor, the icon takes the surrounding text colour. Fix the definition once and every copy updates.
Worked example: an icon sprite with <symbol> and <use>
Define icons once, reuse them at any size and colour
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SVG sprites</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; background:#0f172a; color:#e5e7eb; }
.bar { display:flex; gap:24px; align-items:center; }
/* size and colour each instance independently; currentColor reads color. */
.icon { width:40px; height:40px; }
.blue { color:#3b82f6; }
.green { color:#22c55e; }
.big { width:64px; height:64px; }
</style>
</head>
<
...6. Accessibility — Make Graphics Announceable
A screen reader can't "see" your shapes, so you must describe them. For a meaningful graphic (a logo, an informative chart), add role="img" and an accessible name — the simplest is aria-label="Company logo" on the <svg>. A <title> as the first child also names it and shows as a tooltip; pair it with aria-labelledby for the most reliable support.
For a decorative graphic that adds nothing for a non-sighted user, do the opposite: aria-hidden="true" so the screen reader skips it entirely. The worst option is leaving a meaningful icon unlabelled and silent.
Worked example: meaningful vs decorative
role + aria-label vs <title> vs aria-hidden
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessible SVG</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; background:#0f172a; color:#e5e7eb; }
.row { display:flex; gap:32px; align-items:center; }
svg { width:56px; height:56px; }
</style>
</head>
<body>
<div class="row">
<!-- MEANINGFUL via aria-label: a reader announces "Download, image". -->
<svg viewBox="0 0 24 24" role="img" aria-label="Download"
fill=
...🎯 Your Turn #1 — Draw and colour a shape
Finish this inline SVG so it shows a yellow circle with a smiling mouth. Fill in the blanks marked ___, then run it and check the expected result in the comments.
Your Turn #1: a smiley with circle, fill and a stroked path
Set the viewBox, fill the face, stroke the smile
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn 1</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; background:#0f172a; }
</style>
</head>
<body>
<!-- 🎯 YOUR TURN — fill in the blanks marked ___ -->
<!-- 1) Give the SVG a coordinate grid 100 units wide and tall. -->
<svg viewBox="___" width="200" height="200" role="img" aria-label="Smiley face">
<!-- 👉 viewBox="0 0 100 100" -->
<!-- 2) Draw the face: a circle c
...🎯 Your Turn #2 — Theme with CSS and draw the line
Two jobs: colour the icon from a custom property instead of the markup, and make the underline draw itself. Add the var(), the dash properties, and the keyframe target, then verify against the comments.
Your Turn #2: var() fill + a self-drawing underline
fill: var(--brand) and a stroke-dashoffset animation to 0
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn 2</title>
<style>
/* 🎯 YOUR TURN — fill in the blanks marked ___ */
:root { --brand: #8b5cf6; } /* purple brand token */
body { font-family: system-ui, sans-serif; padding: 24px; background:#0f172a; color:#e5e7eb; }
/* 1) Colour the star from the variable, NOT a hard-coded hex. */
.star { fill: ___; } /* 👉 var(--brand) */
/* 2) Make this line draw itself. Set BOTH
...🧩 Mini-Challenge — An accessible icon sprite from scratch
Support is faded now — only an outline is given. Build a tiny icon sprite and use it twice. Lean on the worked examples in sections 5 and 6 if you get stuck.
Mini-Challenge: symbol + use, themed and accessible
Define one icon, stamp it twice at different colours, label it
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mini-Challenge</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; background:#0f172a; color:#e5e7eb; }
/* 🧩 MINI-CHALLENGE: a reusable, themeable, accessible icon
1. In a hidden <svg> (width=0 height=0, aria-hidden="true"), define a
<symbol id="ic-check" viewBox="0 0 24 24"> containing a tick <path>
that uses fill="none" stroke="currentColor".
2. Stamp it
...⚠️ Common Errors (and the fix)
- Missing viewBox. Without
viewBoxthe browser has no coordinate grid to scale, so the graphic clips or refuses to resize when you change width/height. Fix: addviewBox="0 0 W H"matching the coordinates you drew in (e.g.0 0 24 24). - No accessible name. A meaningful icon with no label is invisible to screen readers — they announce nothing, or read raw markup. Fix: add
role="img"plusaria-label(or a<title>); for decoration usearia-hidden="true". - fill attribute won't change on hover. Setting
color: redon an SVG does nothing — shapes usefill/stroke, notcolordirectly. Fix: stylefillin CSS, or setfill="currentColor"socolordrives it. - Draw effect does nothing. Animating
stroke-dashoffseton a shape with afilland no stroke shows no change. Fix: setfill: none, give it astroke, and makestroke-dasharray≥ the path length. - Using SVG for a photo. SVG describes shapes, not pixels — there's nothing to "describe" in a photograph, so it bloats or can't be done. Fix: use a raster format (
<img>with PNG/JPG/WebP) for photos; keep SVG for icons, logos and line art.
📋 Quick Reference
| Goal | Use this |
|---|---|
| Set the coordinate grid | <svg viewBox="0 0 24 24"> |
| Rectangle / rounded box | <rect x y width height rx /> |
| Circle | <circle cx cy r /> |
| Any shape / line | <path d="M.. L.. Q.. Z" /> |
| Outline only | fill="none" stroke="..." stroke-width="2" |
| Inherit text colour | stroke="currentColor" / fill="currentColor" |
| Theme from CSS | fill: var(--brand); |
| Draw a line on | stroke-dasharray + dashoffset → 0 |
| Reuse an icon | <symbol id> … <use href="#id"/> |
| Accessibility | role="img" aria-label="…" / aria-hidden="true" |
❓ Frequently Asked Questions
What is the difference between SVG and a PNG or JPG?
PNG and JPG are raster formats: they store a fixed grid of coloured pixels, so when you scale them up the browser has to invent pixels and the image goes blurry or blocky. SVG is a vector format: it stores instructions like 'draw a circle of radius 50 here' as text, and the browser redraws those instructions at whatever size you ask for, so it stays razor-sharp at any zoom or screen density. Use SVG for icons, logos, charts, and line art; use PNG or JPG for photographs, where there is no shape to describe — only millions of individual pixels.
Why does my SVG ignore width and height, or look the wrong size?
Almost always a missing or wrong viewBox. The viewBox defines the SVG's internal coordinate grid (its own little world of units); width and height define how big that grid is drawn on the page. Without a viewBox the browser can't map your 0-24 coordinates onto a 96px box, so the graphic gets clipped or sized strangely. Add viewBox="0 0 24 24" (matching the coordinates you drew in) and then width/height — or CSS — can scale it freely.
Should I set colours with the fill attribute or with CSS?
Either works, but CSS wins when there's a conflict, and CSS is what you want for anything that changes — hover, dark mode, theming. A presentation attribute like fill="red" on the element is the weakest source, so a CSS rule (even fill: blue from a class) overrides it. The exception is an inline style attribute, which beats both. The idiomatic pattern for icons is to set fill="none" stroke="currentColor" in the markup and let the parent's CSS color drive the colour.
How does the stroke-dasharray drawing animation actually work?
You turn the line into a dashed line whose single dash is as long as the whole path, then push that dash off the end with stroke-dashoffset so nothing shows. Animating the offset back to 0 slides the dash into view, which looks like the line drawing itself. Set stroke-dasharray and stroke-dashoffset to the path's total length (use getTotalLength() in JS for the exact number, or a value big enough to cover it), then animate stroke-dashoffset to 0. It only works on the stroke, so the shape needs a stroke and usually fill: none.
Should I use CSS animation or SMIL (<animate>) for SVG?
Prefer CSS (and the Web Animations API in JS) for almost everything. SMIL is SVG's built-in animation syntax — tags like <animate> and <animateTransform> embedded right in the markup — and it can animate a few attributes CSS historically couldn't, but it's quirky, harder to control from JavaScript, and was once deprecation-flagged in Chrome. CSS keyframes are better supported, more familiar, and integrate with the rest of your stylesheet, so reach for SMIL only for a specific attribute animation CSS can't express.
How do I make an SVG accessible to screen readers?
If the SVG conveys meaning (a logo, an informative chart), give it role="img" and an accessible name — either aria-label="Company logo" on the <svg>, or a <title> element as the first child (the <title> also shows as a tooltip). If the SVG is purely decorative, hide it instead with aria-hidden="true" so screen readers skip it. Don't rely on a <title> alone without role="img", because support for announcing it is inconsistent across screen readers.
🎉 Lesson Complete
You can now draw, style, animate and label vector graphics in pure markup. The essentials:
- ✅ Inline SVG shapes are DOM elements — styleable, scriptable, inspectable
- ✅
viewBoxsets the coordinate grid; width/height set the pixels — that's what makes SVG resolution-independent - ✅ Draw with
rect/circle/path; paint withfillandstroke - ✅ Style from CSS and theme with
var()/currentColor - ✅
stroke-dasharray+stroke-dashoffset→ 0 makes a line draw itself (prefer CSS over SMIL) - ✅
<symbol>+<use>= define once, reuse everywhere - ✅ Add
role="img"+aria-label(oraria-hiddenfor decoration)
Next up: Responsive Navigation, where you'll build a menu that adapts from desktop bar to mobile drawer.
Sign up for free to track which lessons you've completed and get learning reminders.