How to make an HTML bar chart

A bar chart is a list of values with a width or height proportional to each. CSS draws it from an ordinary table, and a share link means the chart people open is the current one.

An HTML bar chart is a set of elements whose width or height is a percentage of the largest value, and CSS alone is enough to draw it.

Start from a table rather than a stack of empty divs. The numbers then survive with styles disabled, in a text email client, and for a screen reader.

A finished horizontal bar chart page. Category labels left, bars and values right.
A finished horizontal bar chart page. Category labels left, bars and values right.

The markup

Keep the value in the text, and put the proportion in a custom property.

<table class="bars">
  <tr>
    <th>Organic search</th>
    <td><span class="bar" style="--w:100%"></span> 4,120</td>
  </tr>
  <tr>
    <th>Direct</th>
    <td><span class="bar" style="--w:58%"></span> 2,390</td>
  </tr>
</table>

The percentage is the value divided by the largest value, not by the total. Dividing by the total is the most common mistake here and it makes every bar look small.

.bar {
  display: inline-block; width: var(--w);
  height: 0.9rem; background: #4f7cff;
  border-radius: 2px; vertical-align: middle;
}

Horizontal or vertical

Situation Use
Category names are words or phrases Horizontal
The axis is months, weeks, or quarters Vertical
More than about ten categories Horizontal, sorted by value
Comparing two series per category Grouped, either direction
Showing composition within each category Stacked
Values differ by more than 100x Reconsider the chart, or split it

Horizontal bars are underused. The label reads left to right on the same line as its bar, so nothing has to be rotated or truncated.

Vertical bars need a flex container with align-items: flex-end and a height on each bar. Rotated labels are the usual failure, and they are unreadable on a phone.

Sorting and the zero line

Sort by value unless the categories have an inherent order such as time or size band. An unsorted category chart makes the reader do the ranking.

Always start bars at zero. A truncated baseline exaggerates differences, and a reader who notices stops trusting the rest of the page.

If one value dwarfs the others, do not truncate the axis. Split it out and note it, or switch to a log scale and say so in the label.

Stacked and grouped bars

Stacked bars show composition inside each category. Each bar becomes a flex row and each segment gets a flex-basis of its share.

.stack { display: flex; width: var(--w); }
.stack > span { display: block; }

Keep the segment order the same in every bar. If the second segment is a different series in row three, the chart is unreadable even though it looks fine.

Grouped bars put two or three thin bars side by side per category. Past three series, the eye gives up and a small multiple set of separate charts reads better.

A stacked bar chart where each row is split into three colour segments in the same order.
A stacked bar chart where each row is split into three colour segments in the same order.

Labels, legends, and grey printing

Put the value at the end of the bar, not inside it. Inside works until a bar is short and the number overflows.

Use one colour for a single series. Multiple colours on a single-series chart suggests a meaning that is not there.

For multiple series, check the chart still reads printed in grey. Pair each colour with a different fill pattern or keep the lightness clearly separated, since hue alone fails for a meaningful share of readers.

Making it work on a phone

Horizontal bars handle narrow screens almost for free. The label column shrinks and the track shrinks with it.

Below about 500px, move the label above its bar instead of beside it. That buys the bar the full width of the screen.

Vertical bars usually need to become horizontal below the breakpoint. Confirm the viewport meta tag is in the head first, otherwise no media query fires as you expect.

Axis, gridlines, and how much to draw

A bar chart often needs no axis at all. If the value is printed at the end of every bar, an axis is repeating information.

Add gridlines when readers compare bars that are far apart on the page. Keep them faint, behind the bars, and space them at round numbers rather than at whatever divides evenly.

Skip the chart border and the background fill. Both are inherited habits from spreadsheet defaults and neither helps anyone read the data.

Label the unit once, in the heading above the chart. "Sessions, Q3 2026" is better than repeating the word on every row.

Share the HTML bar chart as a page, not a picture

Charts get revised. A picture of a chart does not.

  1. Open the file in the HTML file opener and check the bars and fonts survive outside your folder.
  2. Paste the HTML into a NOS document. It renders as its own page, styles included.
  3. Share, then Share link, then Create link. Leave it unlisted for internal figures.
  4. Send the link and update the page when the figures change.
The share dialog, link created, Public on the web left unticked.
The share dialog, link created, Public on the web left unticked.

Because the address does not move, the link inside March's update still opens the current chart in June. That is the whole argument against a screenshot of a live page.

If the chart is one panel of a larger report, the same route applies to the whole thing. Client dashboards in HTML covers that case.

If an assistant wrote the chart

AI-generated bar charts are usually structurally fine and often scaled wrongly. Check two things.

First, divide by the maximum, not the total. Second, confirm the values in the markup match the values in your source, because a regenerated chart sometimes carries placeholder numbers.

Prompting for an HTML chart covers how to ask for a table-backed chart rather than a canvas one, which is what makes the output editable afterwards.

A value being corrected by clicking the text in the document, with the bar above it.
A value being corrected by clicking the text in the document, with the bar above it.

Before you send it

  • Are bars scaled against the largest value rather than the sum?
  • Does every bar start at zero?
  • Is the numeric value present as text, not only as length?
  • Does the chart read in greyscale?
  • Is there a <title>, and does the link need to stay unlisted?

Questions people ask

Can you make a bar chart with CSS only?

Yes. Set each bar width or height as a percentage of the largest value and CSS does the rest. No JavaScript, no canvas, no library. The markup can stay a real table, so the numbers remain readable when styles do not load.

Horizontal or vertical bars?

Horizontal when the category labels are words, because the label sits on the same line as the bar and never needs rotating. Vertical when the axis is time, because readers expect time to run left to right.

How do I build a stacked bar chart in HTML?

Make each bar a flex container and each segment a child with a flex-basis equal to its share of that bar. Keep the segment order identical across every bar, or readers cannot compare the same series between rows.

How do I share an HTML bar chart?

Paste the HTML into a NOS document and send the share link. The chart renders as a page of its own and the address stays the same when you update the figures, so the link in an old message still shows current numbers.

Keep reading