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: 1means "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).
const width = (value / most) * 100; // a number in, a percentage out03Let 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
Use only 80 percent of the track
In the first example, change
* 100to* 80insideshare. The widths become59.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
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 bybar.style.width = pct + "%";. Theall zero,negativeandmissingrows now printstyle.width = "0%" -> drawn 0 px of 200. The three bars that were wrongly full are now empty. - 3
Chart a different week
In the first example, change the
weekvalues to your own seven numbers. The longest bar becomes 100% by itself, becausemostis found from the data. Nothing else needs to change.
Code & diagrams
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
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%A failure that prints no error: three of the bars are full length although their numbers are zero-by-zero, negative and missing.
Preview
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 14pxBreak 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 %.
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.
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 chartrole="img"and a label, but note the cost: animgrole 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 thanisNaN():isNaN("abc")istrue, butisNaN("5")isfalsebecause it first converts the string, whileNumber.isFinite("5")isfalseand 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
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
A bar is a box whose size comes from a number. A
divwith 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
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 withoverflow: hidden; text-overflow: ellipsis; white-space: nowrap, and atitleattribute keeps the full text available on hover. - 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 thedivkeeps 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, andshare()in the first example is the guard. - 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
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). Everydivis 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
How is the width of each bar worked out in the first example, and why is Tuesday exactly 100%?
Why does a bar for a negative number, a missing number or 0 ÷ 0 come out full length, and how do you prevent it?
Why use a percentage width for a bar inside a track, rather than a pixel width?
When would you choose HTML and CSS bars over an SVG chart, and when not?
Practice
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?
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.widthin 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.