Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~24 min· topic 7 of 7

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: auto in 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. 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. 2

    Recolour the icon without touching the SVG

    In the CSS, change header svg { ... color: #2a9d8f; } to color: #e76f51. The bike turns orange, because its paths use currentColor. One CSS line recolours the whole icon.

  3. 3

    Use the pin again

    Add <svg viewBox="0 0 24 24" width="24" height="24"><use href="#pin"></use></svg> inside the Rides through the year box's <h2>. A second pin appears, with no new path data. Use the same pattern for the 25 stations in Phase 13.

Code & diagrams

The Lumen Bikes dashboard after Phase 1 New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Bars: 7
Tallest bar: 160 (Tuesday)
Chart shape (height / width): 0.553 at any size
Titled boxes: 3

Your project files

Your lumen-dashboard project after this topic. Click a file to see it in full.

index.htmlhtml

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.

terminal
$ Preview and Console
── what you'll see ──
The chart is no longer scaled: the bars keep their full size, the right-hand bars are cut off at the box edge, and the bottom is cut off at the default height of 150. The Console prints `Chart shape (height / width): NaN at any size` because the SVG has no viewBox to read. No error is printed.

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).

terminal
$ Console and screen reader
── what you'll see ──
The Console and the picture are unchanged, with no error. A screen reader announces the chart with only the description, or with no name in some readers, because one of the ids points at nothing.

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 use display: none: Chromium does not paint a gradient defined inside a display: none SVG (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, and aria-labelledby pointing 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. 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 viewBox and gives the page a vector bike icon and a station pin that are sharp at any size. Priya's request is met.

  2. 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 stopgap overflow-x: auto on .box is removed: it hid the problem instead of fixing it.

  3. 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 0 twice makes a full circle), and the frame, fork, handlebar and seat are paths of straight lines with M, L, H and Z. The icon is defined once in a <symbol id="bike"> and drawn with <use>, in currentColor so 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. 4

    Styling moves to CSS. The bar colours are no longer attributes on each rect but classes: .bar and .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 uses currentColor.

  5. 5

    Proper titles. Each box is a <section> with aria-labelledby pointing at its <h2>. The chart has a <title> (a short name) and a <desc> (the seven numbers and the quiet day), linked with aria-labelledby on the <svg>. The <title> also shows as a hover tooltip in most browsers. The self-check at the bottom counts three titled boxes.

  6. 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

01

What changed in the week chart's <svg> tag, and why does that fix the phone problem?

02

How is the bike icon drawn, and why is it defined as a <symbol>?

03

What is still missing from the dashboard, and which phases add it?

Practice

01

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?

02

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.