CSS Bar Chart
At the c't webdev conference I showed yesterday that pure HTML, CSS and JS are quite powerful, hence the name of my talk "View Source is Back - Why framework-less matters in 2025 (and beyond)".
A part of the live-coding (what I like to do) was turning a plain table into a bar chart using nothing but CSS. It's a tiny trick, but a great reminder of how much raw power the web offers.
Contents
- The Table
- The Data
- Step 0 - The Pure Table
- Step 1 - Make the Data more useful
- Step 2 - A Horizontal Bar Chart
- Step 3 - The Real Bar Chart
- Step 4 - The Y-Axis
- Conclusion
The Table
A bit of structure is needed, and also helps a lot with turning a table into a bar chart. A very simple table may look like this:
<table>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
We can give it a lot more structure with the following tags:
<table>
<caption></caption>
<colgroup></colgroup>
<thead></thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
You can read all details best on MDN no need to duplicate them here.
We will make use of some of them to structure the table data, which makes them very accessible and by doing so it will also be much easier to apply a couple lines of CSS to "convert" this table into a bar chart.
The Data
Here I focus on the least data we need to get a bar chart. The table allows for much more data to be given and also usefully use for a bar chart. But we reduce the complexity.
<table>
<thead>
<tr>
<th>Month</th>
<th>Clicks</th>
</tr>
</thead>
<tbody>
<tr>
<th>January 2025</th>
<td style="--percent: 54.7619%;">23</td>
</tr>
<tr>
<th>February 2025</th>
<td style="--percent: 100%;">42</td>
</tr>
...
</tbody>
</table>
Let's dive into the structure.
We use <thead> and <tbody> to explicitly separate the data from the header.
Each row consists of a <th> which is the label which the data is for. So line 1 shows the data for January 2025, the value is 23 which is noted in the <td> tag. Note the two different tags td and th here we just use them semantically, by having a meaning we do not only make the data more accessible but also easier to process them any further, which in our case is just styling via CSS.
The style="--percent: 54.7619%;" attribute is the only special thing here, which you may want to generate on the server and put into the markup like this. We will need this for the height of the bar in the bar chart later on. It is the percentage of the value shown in the <td> but as a percentage value relative to the max value in the table.
Step 0 - The Pure Table
The table as described above render like so.
| Month | Clicks |
|---|---|
| January 2025 | 23 |
| February 2025 | 42 |
| March 2025 | 40 |
| April 2025 | 12 |
Step 1 - Make the Data more useful
By adding one simple background: linear-gradient we can make the data already a lot more useful.
| Month | Clicks |
|---|---|
| January 2025 | 23 |
| February 2025 | 42 |
| March 2025 | 40 |
| April 2025 | 12 |
This isn't a pure bar chart yet, but the data is already much more useful — the rows are visualized in a way that makes it far easier to see how their values relate to each other.
It only takes one line of CSS to transform the table: we set the background color to a distinct color up to whatever --percent specifies. Now each row looks like a bar, making them easy to compare.
tbody {
td {
background: linear-gradient(to right, lightgrey var(--percent), transparent 0);
}
}
Depending on one's needs, you can stop here. The data have become quite useful. But let's take it a step further.
Step 2 - A Horizontal Bar Chart
The bar chart shall render from left to right and we want real bars going from bottom to top. So we will make two simple changes, one is to align the <tr>s inside the <tbody>, our "data element" aside one another (not below, as they are), and second we will make the background gradient go from bottom to top.
| Month | Clicks |
|---|---|
| January 2025 | 23 |
| February 2025 | 42 |
| March 2025 | 40 |
| April 2025 | 12 |
tbody {
display: flex;
td {
background: linear-gradient(to top, lightgrey var(--percent), transparent 0);
}
}
We make use of CSS nesting, just like in programming the blocks apply to their parent context, so the td we style the background for does only apply when it is inside a tbody, this makes CSS less verbose and also easier to read and maintain.
As of November 2025 91% of users have devices that support CSS nesting, caniuse.com says.
If we want to flip the order of the data, we can add tbody { flex-direction: row-reverse; } to render
April first and January last.
Instead of
linear-gradient(to right,
we changed it to
linear-gradient(to top,
so the "bar" goes from bottom to top. Isn't CSS lovely?
Let us do one third thing on the fly, let us thead { display: none; } so we do not see the "Month" and "Clicks" since now, they do not make much sense anymore.
Step 3 - The Real Bar Chart
I am sure it's itching to remove the <th> with the month in there. And yes, we will touch it. But not remove it. We will do five tiny things with it:
th {
position: absolute;
writing-mode: vertical-lr;
white-space: nowrap;
text-shadow: 0 0 2px black;
color: white;
}
The position: absolute; makes that this element is "taken out of the flow" and basically occupies no space anymore, and viola the bars are one aside the other.
The writing-mode: vertical-lr; makes the "February 2025" text "turn" around by 90 degrees to be readable from top to bottom, so it will be kinda written on the bar, with white-space: nowrap; we just ensure the text is not being wrapped, which would make it ugly to read.
The two instructions text-shadow: 0 0 2px black; color: white; make sure the text has a shadow, which makes it readable on top of any background, no matter if dark or light. So when the text "runs out of the bar" it is still easy to read.
tbody {
position: relative;
td {
height: 10em;
}
th {
height: 100%;
text-align: end;
padding: 0.25em;
}
}
To make all the text well readable and also the bars better visible, we just make them higher and black. The <tbody> needs to be position: relative so our position: absolute for the <th>'s does align the label text inside the <tbody> which is basically the area of our bar chart.
| Month | Clicks |
|---|---|
| January 2025 | 23 |
| February 2025 | 42 |
| March 2025 | 40 |
| April 2025 | 12 |
| May 2025 | 0 |
| June 2025 | 1 |
| July 2025 | 13 |
| August 2025 | 30 |
| September 2025 | 35 |
| October 2025 | 36 |
| November 2025 | 31 |
| December 2025 | 24 |
I also added a couple more columns. Bar charts just look nicer with more data.
Step 4 - The Y-Axis
We add to the table a <tfoot> section, which we will later use as our y-axis. The footer is formatted in a special way, so we can use it as y-axis, but that is the case with our data too. So there are conventions that we stick to otherwise it does not work, but that's always the case, right?
<tfoot>
<tr>
<th>min</th>
<td style="--percent: 0%;">0</td>
</tr>
<tr>
<th>middle</th>
<td style="--percent: 50%;">21</td>
</tr>
<tr>
<th>max</th>
<td style="--percent: 100%;">42</td>
</tr>
</tfoot>
To make this footer show up on the left of the chart, we use display:flex again and the
flex-direction: row-reverse;, see:
table {
display: flex;
flex-direction: row-reverse;
}
This sorts the children of <table> to be in the order <tfoot> <tbody>, now the y-axis can be drawn.
tfoot {
display: flex;
flex-direction: column-reverse;
justify-content: space-between;
tr {
display: flex;
}
th {
display: none;
}
td {
flex: 1;
border: none;
font-weight: normal;
font-size: small;
}
}
The justify-content: space-between; will just make the three y-axis labels show up as far apart as possible, therefore aligning them top, middle and bottom. So this quite a strong dependency between our data and the CSS.
| Month | Clicks |
|---|---|
| January 2025 | 23 |
| February 2025 | 42 |
| March 2025 | 40 |
| April 2025 | 12 |
| May 2025 | 0 |
| June 2025 | 1 |
| July 2025 | 13 |
| August 2025 | 30 |
| September 2025 | 35 |
| October 2025 | 36 |
| November 2025 | 31 |
| December 2025 | 24 |
| min | 0 |
| middle | 21 |
| max | 42 |
As you can probably notice, a couple of polishings have found their way into this bar chart too. I added some margin and paddings around the <tbody> so the y-axis aligns better, a background color for the data and some alignment for the labels.
Conclusion
It just takes about 40 lines of CSS to make a table into a bar chart. This is nothing else but styling data, if HTML (or browsers) say tabular data have to look a certain way it's our responsibility to ask "why not different?".
Done hereby.
Find this and other techniques in action come to SEO Rank, a service we are building to easier understand SEO data for your site.