Topic 1.7
Build: The Dashboard Frame in SVG
In one line
Rebuild the dashboard frame so it fits any screen: a header with a bike icon made of paths, three titled boxes, and the week chart in a viewBox that scales from a phone to a wall screen. It answers Priya's request for a sharp, fitting dashboard.
Before you start
You will learn
- Make a drawing scale to its box with a viewBox and no fixed size
- Draw an icon from paths, and reuse it with symbol and use
- Give each box and chart an accessible title
You write code in
In your own lumen-dashboard/index.html (replace its contents), and in the editor below
Files you touch
- index.html
Think of it like this
A poster printer. You give it one design, and it can print a postcard or a wall banner without redrawing anything, because the design is made of shapes, not of dots. Priya wants the dashboard to behave like that, on her phone and on the control-room wall.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- responsive
- Changing with the screen size. The week chart is responsive because its viewBox scales it to its box.
- symbol
- A drawing defined once with its own viewBox, drawn by
<use>. The bike and the pin are symbols. - <title> and <desc>
- A short name and a longer description inside an SVG. Assistive tools read them, and browsers show the title as a tooltip.
- aria-labelledby
- An attribute that names an element by pointing at the id of another element that holds the name.
- stopgap
- A quick fix that hides a problem without solving it, like
overflow-x: autoin Phase 0.
Step by step
01Start from the Phase 0 week chart
Your lumen-dashboard/index.html has a header, three boxes and a week chart that is 380 pixels wide. Here is that chart on its own in a narrow box. Its fixed width="380" is wider than the 260-pixel box, so part of it is cut off and the box scrolls.
02Replace the fixed size with a viewBox
Delete width="380" height="210" and write width="100%" viewBox="0 0 380 210" instead. The coordinates inside stay exactly as they were. The chart now fits the 260-pixel box, scaled by 260 ÷ 380 = 0.68, and the box no longer scrolls.
03Check the same chart in a wider box
The same markup in a 520-pixel box: scale 1.37, height about 287 (520 × 210 ÷ 380). Both boxes show the same picture. This is what the phone and the wall screen get from one chart.
04Draw the bike from paths
The icon is four paths drawn in a 48 by 32 box; its viewBox (-2 -2 52 36) adds 2 units all round, because a stroke is centred on the edge and would otherwise be cut off (Topic 1.5). The wheels: M 4 24 a 8 8 0 1 0 16 0 a 8 8 0 1 0 -16 0, a circle made of two half-arcs. The frame: M 12 24 L 19 10 L 32 10 L 24 24 Z. The fork, handlebar and seat: M 32 10 L 36 24 M 29 6 H 35 M 16 7 H 22. All use stroke="currentColor" and fill="none", with round caps so the ends look soft.
05Name every box and the chart
Each <section> gets aria-labelledby="week-heading" (and so on) pointing at its <h2>. The chart <svg> gets role="img" and aria-labelledby="week-title week-desc", pointing at a <title> and a <desc> inside it. The old long aria-label text moves into the <desc>.
The self-check prints the chart's shape, 0.553, which is 210 ÷ 380: the number that stays the same at every width.
06Replace your file and test three widths
Replace your index.html with the file in the Files section (or the live example below). Open it, then drag the browser window from wide to narrow, or use the phone view of the developer tools (F12). The three boxes stack, and the week chart grows and shrinks with its box. There is no horizontal scroll bar. The Console should say Bars: 7, Tallest bar: 160 (Tuesday), Chart shape (height / width): 0.553 at any size and Titled boxes: 3.
Try it yourself
- 1
Check the chart at phone width
Make the preview as narrow as it goes (or use your browser's phone view). The boxes stack in one column, and the week chart fills the width. The Console still prints
0.553, because the shape never changes. Text inside the chart gets smaller with it: that is the price of a viewBox. - 2
Recolour the icon without touching the SVG
In the CSS, change
header svg { ... color: #2a9d8f; }tocolor: #e76f51. The bike turns orange, because its paths usecurrentColor. One CSS line recolours the whole icon. - 3
Use the pin again
Add
<svg viewBox="0 0 24 24" width="24" height="24"><use href="#pin"></use></svg>inside theRides through the yearbox's<h2>. A second pin appears, with no new path data. Use the same pattern for the 25 stations in Phase 13.
Code & diagrams
This is the whole lumen-dashboard/index.html after Phase 1, exactly as in the Files section. Make the preview narrow and wide to watch the week chart scale. No imports are needed: this phase uses plain SVG and CSS.
Preview
Console
Nothing logged yet.Expected console output
Bars: 7
Tallest bar: 160 (Tuesday)
Chart shape (height / width): 0.553 at any size
Titled boxes: 3Your project files
Your lumen-dashboard project after this topic. Click a file to see it in full.
The whole file after this topic. If yours differs, you can copy this one and carry on.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lumen Bikes dashboard</title>
<style>
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f6f7f9; color: #1d2b36; }
header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
header svg { flex: none; width: 56px; height: auto; color: #2a9d8f; }
header h1 { margin: 0; font-size: 22px; }
header p { margin: 2px 0 0; color: #5b6b78; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; }
.box { background: white; border: 1px solid #dde3e8; border-radius: 8px; padding: 12px; }
.box h2 { margin: 0 0 8px; font-size: 15px; display: flex; align-items: center; gap: 6px; }
.box h2 svg { width: 18px; height: 18px; fill: #e76f51; }
.soon { color: #5b6b78; font-size: 14px; }
/* the week chart: one viewBox, so it scales with its box */
.chart { display: block; width: 100%; height: auto; }
.chart .bar { fill: #2a9d8f; }
.chart .bar.quiet { fill: #e76f51; }
.chart .baseline { stroke: #9aa8b4; }
.chart .labels { font-size: 12px; text-anchor: middle; fill: currentColor; }
</style>
</head>
<body>
<!-- Shapes defined once and reused with <use> -->
<svg width="0" height="0" style="position: absolute" aria-hidden="true">
<defs>
<symbol id="bike" viewBox="-2 -2 52 36">
<g fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M 4 24 a 8 8 0 1 0 16 0 a 8 8 0 1 0 -16 0"></path>
<path d="M 28 24 a 8 8 0 1 0 16 0 a 8 8 0 1 0 -16 0"></path>
<path d="M 12 24 L 19 10 L 32 10 L 24 24 Z"></path>
<path d="M 32 10 L 36 24 M 29 6 H 35 M 16 7 H 22"></path>
</g>
</symbol>
<symbol id="pin" viewBox="0 0 24 24">
<path d="M12 2 C8 2 5 5 5 9 C5 14 12 22 12 22 C12 22 19 14 19 9 C19 5 16 2 12 2 Z"></path>
<circle cx="12" cy="9" r="2.5" fill="#fff"></circle>
</symbol>
</defs>
</svg>
<header>
<svg viewBox="-2 -2 52 36" role="img" aria-label="Lumen Bikes: a bicycle">
<use href="#bike"></use>
</svg>
<div>
<h1>Lumen Bikes</h1>
<p>Ardenvale bike share, operations dashboard</p>
</div>
</header>
<main class="grid">
<section class="box" id="this-week" aria-labelledby="week-heading">
<h2 id="week-heading">Rides this week (2–8 June 2025)</h2>
<!-- 7 bars drawn by hand: height = rides ÷ 3,992 × 160 (the busiest day is 160 tall) -->
<svg class="chart" viewBox="0 0 380 210" role="img" aria-labelledby="week-title week-desc">
<title id="week-title">Rides per day, 2–8 June 2025</title>
<desc id="week-desc">Monday 2,979, Tuesday 3,992, Wednesday 3,759, Thursday 3,742, Friday 3,648, Saturday 3,013, Sunday 3,451. Monday, a rainy day, was the quietest.</desc>
<line class="baseline" x1="20" y1="180" x2="380" y2="180"></line>
<rect class="bar quiet" x="30" y="60.6" width="40" height="119.4"></rect>
<rect class="bar" x="80" y="20" width="40" height="160"></rect>
<rect class="bar" x="130" y="29.3" width="40" height="150.7"></rect>
<rect class="bar" x="180" y="30" width="40" height="150"></rect>
<rect class="bar" x="230" y="33.8" width="40" height="146.2"></rect>
<rect class="bar" x="280" y="59.2" width="40" height="120.8"></rect>
<rect class="bar" x="330" y="41.7" width="40" height="138.3"></rect>
<g class="labels">
<text x="50" y="198">Mon</text><text x="100" y="198">Tue</text><text x="150" y="198">Wed</text>
<text x="200" y="198">Thu</text><text x="250" y="198">Fri</text><text x="300" y="198">Sat</text>
<text x="350" y="198">Sun</text>
</g>
</svg>
</section>
<section class="box" id="rides-line" aria-labelledby="line-heading">
<h2 id="line-heading">Rides through the year</h2>
<p class="soon">Coming in Phase 3: a line of all 365 days.</p>
</section>
<section class="box" id="stations-bar" aria-labelledby="stations-heading">
<h2 id="stations-heading">
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#pin"></use></svg>
Busiest stations
</h2>
<p class="soon">Coming in Phase 5: bars that update with the data.</p>
</section>
</main>
<script>
// A quick self-check: bars, the chart's shape and the titled boxes
const bars = document.querySelectorAll("#this-week .bar");
const heights = Array.from(bars, (b) => Number(b.getAttribute("height")));
console.log("Bars:", bars.length);
console.log("Tallest bar:", Math.max(...heights), "(Tuesday)");
const view = document.querySelector("#this-week svg").viewBox.baseVal;
console.log("Chart shape (height / width):", (view.height / view.width).toFixed(3), "at any size");
console.log("Titled boxes:", document.querySelectorAll(".box[aria-labelledby]").length);
</script>
</body>
</html>Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Delete the viewBox
In the week chart's <svg>, delete viewBox="0 0 380 210" (keep the CSS width: 100%; height: auto) and make the preview phone-narrow.
Break #2
Name the chart wrongly
In the week chart, change aria-labelledby="week-title week-desc" to aria-labelledby="week-titel week-desc" (a typo in the first id).
Myth vs fact
Myth
Making a chart responsive means writing media queries for every size.
Fact
A viewBox with width: 100% scales one drawing to every size. Media queries are for changing the layout, not for scaling a picture.
Myth
An icon in an SVG is a picture, so it cannot change colour.
Fact
With currentColor and a symbol, one CSS line recolours it. Because it is made of paths, it is sharp at any size.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The hidden definitions
<svg width="0" height="0" style="position: absolute">must not usedisplay: none: Chromium does not paint a gradient defined inside adisplay: noneSVG (a shape filled with it shows no fill at all), while a<use>of a symbol from the same hidden SVG still draws, so the bug is easy to miss. Hiding it by size keeps every reference working. - ▸
The chart's text scales with the viewBox, so at phone width the 12-unit labels are about 8 pixels. Keeping text readable at every width needs a chart that measures its box and redraws with its own coordinates (Phase 10). This build is the right trade for a fixed-shape picture of seven bars.
- ▸
<title>as the first child of an SVG is the standard way to name it, andaria-labelledbypointing at the<title>and<desc>is the most widely supported pattern. Phase 15 tests it with real screen readers and builds a data table fallback.
Remember this
- 1
The problem this solves: after Phase 0 the week chart had a fixed width of 380 pixels. On Priya's phone the box scrolled sideways, and on the wall screen the chart stayed small. Her icons also needed to stay sharp when zoomed. This build fixes the first problem with a
viewBoxand gives the page a vector bike icon and a station pin that are sharp at any size. Priya's request is met. - 2
What changes. The week chart is now
<svg viewBox="0 0 380 210">with a width of 100% and no height: the browser scales the same 380 by 210 drawing to whatever width the box has, and works out the height from the shape (about 0.553 of the width). Every coordinate inside it is unchanged from Phase 0, so none of the bar arithmetic is touched. The stopgapoverflow-x: autoon.boxis removed: it hid the problem instead of fixing it. - 3
The bike icon. The header holds an SVG of 48 by 32 units. Its two wheels are circles written as arc paths (
a 8 8 0 1 0 16 0twice makes a full circle), and the frame, fork, handlebar and seat are paths of straight lines withM,L,HandZ. The icon is defined once in a<symbol id="bike">and drawn with<use>, incurrentColorso it follows the text colour. A second symbol,pin, is the station marker used next to "Busiest stations". Both are defined in a hidden<svg>at the top of the page. - 4
Styling moves to CSS. The bar colours are no longer attributes on each rect but classes:
.barand.bar.quiet(Monday, the rainy quiet day). Phase 10 will recolour the same classes for dark mode without touching the SVG. Label text is anchored by CSS too (text-anchor: middle), and usescurrentColor. - 5
Proper titles. Each box is a
<section>witharia-labelledbypointing at its<h2>. The chart has a<title>(a short name) and a<desc>(the seven numbers and the quiet day), linked witharia-labelledbyon the<svg>. The<title>also shows as a hover tooltip in most browsers. The self-check at the bottom counts three titled boxes. - 6
What the dashboard can do now that it could not before: it fits any screen from a phone to a wall, with no sideways scrolling; it has a vector logo and station icon; and each box and chart has a proper name for assistive tools. Still missing: the bars are still typed by hand (Phase 3 draws them from data in code), the chart has no axes or numbers (Phase 6), the other two boxes are empty (Phases 3 and 5), the colours are fixed (themes, Phase 10), and text shrinks with the drawing on a phone: a limit of the viewBox approach that Phase 10 solves.
Explain it without notes
What changed in the week chart's <svg> tag, and why does that fix the phone problem?
How is the bike icon drawn, and why is it defined as a <symbol>?
What is still missing from the dashboard, and which phases add it?
Practice
Add a fourth box with id status and a heading, Live station status, labelled the same way (aria-labelledby). Where does its <h2> need an id?
The week chart's viewBox is 0 0 380 210. If its box is 190 pixels wide, what are the chart's height and scale? And at 760 pixels?
Trade-offs
- ↔
A viewBox gives a one-line responsive chart but scales text with it. Redrawing at the box's size keeps text readable and costs more code. For a chart with seven bars and short labels, the viewBox is enough.
Done when you can
My lumen-dashboard/index.html has the header with the bike icon and three titled boxes.
The week chart scales with its box and the page never scrolls sideways.
The Console prints Bars: 7 and Chart shape 0.553, with no red lines.
I can explain what changed in the chart's tag and why.
I can say what is still missing and which phases add it.