Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~27 min· topic 1 of 6

Topic 3.1

Bars with HTML and CSS

In one line

A bar chart can be a stack of coloured boxes whose width comes from a number. HTML and CSS draw it with tools you already know, and it is often all a small chart needs. It also teaches the first rule of every chart: turn a number into a size.

Think of it like this

Strips of coloured paper pinned to a wall, each cut to a length that matches a number. You need no drawing skill, only scissors and a ruler. An HTML bar is the same: a div is a strip of paper, and its width is where you cut it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

div
A plain HTML box with no look of its own. Give it a size and a background colour and it is a rectangle.
track
The grey box a bar sits in. The bar's width is a percentage of the track, so 100% means "all the way across".
percentage width
A width written as 50%: half of the parent box's width. It changes by itself when the parent changes.
flexbox
A CSS layout (display: flex) that puts children in a row and shares out the free space. flex: 1 means "take whatever is left".
ellipsis
The three dots (…) that CSS adds when text is too long for its box and has been cut off.
invalid value
A CSS value the browser cannot read, such as NaN% or -20% for a width. The browser ignores it without any message.
sparkline
A tiny chart with no axes, small enough to sit inside a sentence or a table cell.

Step by step

01Start with one week of numbers

The first example uses the seven days of rides from 2 to 8 June 2025 (the same week as the dashboard's first chart). Each row of data has a label (the day) and a value (the rides).

Monday has the flag low: true, because it was the quietest day and it rained. That row gets an orange bar, so the eye goes there first.

02Turn a number into a percentage

The busiest day is Tuesday with 3,992 rides, so Tuesday's bar is 100%. Monday's is 2979 ÷ 3992 × 100 = 74.6%. The function share(value, most) does this and returns 0 for anything that cannot be drawn honestly (not a number, or the biggest value is 0 or less).

the whole scale, in one linewhole filejs
const width = (value / most) * 100; // a number in, a percentage out

03Let flexbox do the layout

Each row is display: flex with a label, a track and a value. The label has a fixed width, the track has flex: 1, so it takes the rest, and the bar is a child of the track with the percentage width. The browser lines up the rows: you never write an x or y.

04Check the awkward inputs on purpose

The first example also renders four small data sets into the same container and logs the widths: an empty array (a message appears instead of bars), one value (100%), two equal values (both 100%), and all zeros (0% each, no division by zero because most <= 0 returns 0).

05Watch a bad number fail without an error

The second example sets the width carelessly, with no checks. A normal 50% gives a half bar, and a zero gives no bar. But 0 / 0, a negative value and undefined all leave style.width empty, and the bar fills the whole 200-pixel track. Nothing in the Console warns you.

06Put a chart inside a sentence

At the end of the second example, seven span boxes (6 pixels wide, display: inline-block) have heights from 0 to 16 pixels. Inline-block boxes sit on the text baseline, so the little bars stand on the line of text like letters. That is a sparkline in HTML and CSS.

07Decide when to stop

If you need an axis, a line, dots, or a scale that someone can read, switch to SVG (Topic 3.2 onwards). If you need a ranked list or a progress indicator, stay here: it is less code, it is real text, and it reflows by itself.

Try it yourself

  1. 1

    Use only 80 percent of the track

    In the first example, change * 100 to * 80 inside share. The widths become 59.7% 80% 75.3% 75% 73.1% 60.4% 69.2%: every bar shrinks by the same factor, and Monday is still the shortest. The scale is only a rule; change the rule, and the whole chart changes together.

  2. 2

    Guard the careless bar

    In the second example, replace the careless line with const pct = Number.isFinite(value / most) ? Math.max(0, (value / most) * 100) : 0; followed by bar.style.width = pct + "%";. The all zero, negative and missing rows now print style.width = "0%" -> drawn 0 px of 200. The three bars that were wrongly full are now empty.

  3. 3

    Chart a different week

    In the first example, change the week values to your own seven numbers. The longest bar becomes 100% by itself, because most is found from the data. Nothing else needs to change.

Code & diagrams

Bars from divs and flexbox, with the awkward inputs New tab

No library: a bar is a div, and its width is a percentage. The empty and all-zero cases are handled in share() and renderBars().

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

week:       74.6% 100% 94.2% 93.7% 91.4% 75.5% 86.4%
empty:      (no bars) | message: No rides to show.
one value:  100%
all equal:  100% 100%
all zero:   0% 0%
When a bar has no honest width: a silent failure, and a sparkline New tab

A failure that prints no error: three of the bars are full length although their numbers are zero-by-zero, negative and missing.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

normal     style.width = "50%" -> drawn 100 px of 200
zero value style.width = "0%" -> drawn 0 px of 200
all zero   style.width = "" -> drawn 200 px of 200
negative   style.width = "" -> drawn 200 px of 200
missing    style.width = "" -> drawn 200 px of 200
long label style.width = "40%" -> drawn 80 px of 200
Sparkline heights: 12px 16px 15px 15px 15px 12px 14px

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

Forget the unit

In the second example, replace the careless line with bar.setAttribute("style", "width:" + (value / most) * 100);. The number is now written as width:50 with no %.

terminal
$ Console
── what you'll see ──
normal style.width = "" -> drawn 200 px of 200
zero value style.width = "0px" -> drawn 0 px of 200
all zero style.width = "" -> drawn 200 px of 200
negative style.width = "" -> drawn 200 px of 200
missing style.width = "" -> drawn 200 px of 200
long label style.width = "" -> drawn 200 px of 200

Break #2

Let a bar grow past 100 percent

In the first example, replace const most = Math.max(...rows.map((r) => r.value)); with const most = 3000;, a number picked by hand that is smaller than the real maximum.

terminal
$ Console
── what you'll see ──
week: 99.3% 133.1% 125.3% 124.7% 121.6% 100.4% 115%
empty: (no bars) | message: No rides to show.
one value: 99.3%
all equal: 66.7% 66.7%
all zero: 0% 0%

Myth vs fact

Myth

A real chart needs a drawing library, and bars made of divs are a cheat.

Fact

A bar chart is shapes sized by numbers. Divs are shapes. For a ranked list, a progress bar or a table with bars in it, HTML and CSS are a good choice: less code, real text, automatic layout. Libraries earn their place when you need axes, lines and scales.

Myth

If the code has no error, the chart is right.

Fact

The second example prints nothing wrong, yet three bars are full length for zero, negative and missing values. CSS ignores values it cannot read. Checking the data (and reading the width back) is your job.

Myth

Percent widths and pixel widths are the same thing.

Fact

A percentage depends on the parent box, so the bar follows the page. A pixel width does not. A percentage is also what you get from "value ÷ biggest", which is why it is the natural unit for bars in a track.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A bar width written as a percentage is resolved by layout, every time the track changes size. SVG bars use a coordinate system that the browser scales as a whole, so no per-bar layout is needed. For a few dozen bars you will not notice the difference; with thousands, you would.

  • ▸

    <progress> and <meter> are real elements with built-in roles: a screen reader announces "progress bar, 75 percent" without any ARIA. A div bar has no meaning of its own. The first example gives the whole chart role="img" and a label, but note the cost: an img role hides everything inside it from a screen reader, including the numbers in the rows. Without the role, the label and number text in each row is read in order, which is the most reliable way to keep an HTML chart accessible (Phase 15 goes deeper).

  • ▸

    Number.isFinite() is stricter than isNaN(): isNaN("abc") is true, but isNaN("5") is false because it first converts the string, while Number.isFinite("5") is false and never converts. For chart values you want the strict test, because a string that sneaks in from a CSV file is a bug you want to catch.

Remember this

  1. 1

    The problem this solves: before you draw with SVG or a library, find out how much you really need. A progress bar, a ranked list of ten stations, a number with a small bar behind it: these are coloured boxes with a width. HTML and CSS handle them, they resize with the page, and the numbers stay real text that people can select and screen readers can read.

  2. 2

    A bar is a box whose size comes from a number. A div with a background colour is a rectangle. You give it a width as a percentage of its track (the grey box it sits in): value ÷ biggest value × 100. The busiest day gets 100 percent, and every other day gets a share of that. Because the width is a percentage, the bar shrinks and grows with the page without any extra code.

  3. 3

    Flexbox does the positioning, so there is not a single x or y coordinate. Each row is a flex container with three children: the label, the track (flex: 1, so it takes the free space) and the number. Every row lines up with every other row. Long labels are cut with overflow: hidden; text-overflow: ellipsis; white-space: nowrap, and a title attribute keeps the full text available on hover.

  4. 4

    Bad numbers fail silently, and they fail big. If the code sets style.width = "NaN%", the browser does not print an error. It treats the value as invalid CSS and ignores it, so the div keeps its default width, which for a block box is the whole track. A zero-by-zero share (NaN), a negative number and a missing value (undefined) all end up as full-length bars: the worst data draws the longest bar. The second example shows it, and share() in the first example is the guard.

  5. 5

    Know when this is enough. HTML bars suit ranked lists, bars inside table cells, progress and a small sparkline inside a sentence (tiny inline boxes, shown in the second example). The browser even has two built-in elements for single values: <progress> (how far along a task is) and <meter> (a value on a known scale, such as a battery level).

  6. 6

    Know where it stops. There is no axis, no line, no scatter, no diagonal or curved shape, and nothing tells you the scale behind the bars. Vertical bars need extra tricks (align-items: flex-end). Every div is also a layout box the browser must size and place, which becomes a cost when there are thousands of them (Topic 12.1 measures where that happens). SVG, the next topics, has none of these limits.

Explain it without notes

01

How is the width of each bar worked out in the first example, and why is Tuesday exactly 100%?

02

Why does a bar for a negative number, a missing number or 0 ÷ 0 come out full length, and how do you prevent it?

03

Why use a percentage width for a bar inside a track, rather than a pixel width?

04

When would you choose HTML and CSS bars over an SVG chart, and when not?

Practice

01

Add a "how it looks with 25 stations" case: render 25 rows (value i * 100, label Station i) and say what happens to the page. What would you do about it?

02

Write shareLabel(value, most) that returns the string "74.6%" for a normal bar, "no data" when the value is null or undefined, and "0%" for 0. Show three calls.

Trade-offs

  • ↔

    HTML and CSS bars are the quickest to write, resize by themselves and are real text, but they have no axes and no lines. SVG needs more code and gives you any shape and a visible scale.

  • ↔

    A percentage width makes the chart responsive for free. The price is that you lose the exact pixel length: if you need to align bars with something else (an axis), you need SVG with a scale.

Done when you can

  • I can make a bar chart from divs, with a track, a label and a number in each row.

  • I can work out a bar's percentage width from its value and the biggest value.

  • I can name what happens to a CSS bar for NaN, negative, missing and zero values, and how to guard each one.

  • I can read element.style.width in the Console to see whether the browser rejected my value.

  • I can say when HTML and CSS bars are enough, and when I need SVG.