I loved Robin’s recent post, experimenting with CSS Grid for bar-charts. I’ve actually been using a similar approach on a client project, building a day-planner with CSS Grid. It’s a different use-case, but the same basic technique: using grid layouts to visualize data.
(I recommend reading Robin’s article first, since I’m building on top of his chart.)
Robin’s approach relies on a large Sass loop to generate 100 potential class-names, even though less than 12 are used in the final chart. In production we’ll want something more direct and performant, with better semantics, so I turned to definition lists and CSS Variables (aka Custom Properties) to build my charts.
Here’s the final result:
See the Pen Bar chart in CSS grid + variables by Miriam Suzanne (@mirisuzanne) on CodePen.
Let’s dig into it!
Markup First
Robin was proposing a conceptual experiment, so he left out many real-life data and accessibility concerns. Since I’m aiming for (fictional) production code, I want to make sure it will be semantic and accessible. I borrowed the year-axis idea from a comment on Robin’s charts, and moved everything into a definition list. Each year is associated with a corresponding percentage in the list:
<dl class="chart">
<dt class="date">2000</dt>
<dd class="bar">45%</dd>
<dt class="date">2001</dt>
<dd class="bar">100%</dd>
<!-- etc… -->
</dl>
There are likely other ways to mark this up accessibly, but a dl
seemed clean and clear to me – with all the data and associated pairs available as structured text. By default, this displays year/percentage pairs in a readable format. Now we have to make it beautiful.
Grid Setup
I started from Robin’s grid, but my markup requires an extra row for the .date
elements. I add that to the end of my grid-template-rows
, and place my date/bar elements appropriately:
.chart {
display: grid;
grid-auto-columns: 1fr;
grid-template-rows: repeat(100, 1fr) 1.4rem;
grid-column-gap: 5px;
}
.date {
/* fill the bottom row */
grid-row-start: -2;
}
.bar {
/* end before the bottom row */
grid-row-end: -2;
}
Normally, I would use auto
for that final row, but I needed an explicit height to make the background-grid work properly. Not not worth the trade-off, probably, but I was having fun.
Passing Data to CSS
At this point, CSS has no access to the relevant numbers for styling a chart. We have no hooks for setting individual bars to different heights. Robin’s solution involves individual class-names for every bar-value, with a Sass to loop to create custom classes for each value. That works, but we end up with a long list of classes we may never use. Is there a way to pass data into CSS more directly?
The most direct approach might be an inline style:
<dd class="bar" style="grid-row-start: 56">45%</dd>
The start position is the full number of grid lines (one more than the number of rows, or 101 in this case), minus the total value of the given bar: 101 - 45 = 56
. That works fine, but now our markup and CSS are tightly coupled. With CSS Variables, we can pass in raw data, and let the CSS decide how it is used:
<dd class="bar" style="--start: 56">45%</dd>
In the CSS we can wire that up to grid-row-start
:
.bar {
grid-row-start: var(--start);
}
We’ve replaced the class-name loop, and bloated 100-class output, with a single line of dynamic CSS. Variables also remove the danger normally associated with CSS-in-HTML. While an inline property like grid-row-start
will be nearly impossible to override from a CSS file, the inline variable can simply be ignored by CSS. There are no specificity/cascade issues to worry about.
Data-Driven Backgrounds
As a bonus, we can do more with the data than simply provide a grid-position – reusing it to style a fallback option, or even adjust the bar colors based on that same data:
.bar {
background-image: linear-gradient(to right, green, yellow, orange, red);
background-size: 1600% 100%;
/* turn the start value into a percentage for position on the gradient */
background-position: calc(var(--start) * 1%) 0;
}
I started with a horizontal background gradient from green to yellow, orange, and then red. Then I used background-size
to make the gradient much wider than the bar – at least 200% per color (800%). Larger gradient-widths will make the fade less visible, so I went with 1600% to keep it subtle. Finally, using calc()
to convert our start position (1-100) into a percentage, I can adjust the background position left-or-right based on the value – showing a different color depending on the percentage.
The background grid is also generated using variables and background-gradients. Sadly, subpixel rounding makes it a bit unreliable, but you can play with the --line-every
value to change the level of detail. Take a look around, and see what other improvements you can make!
Adding Scale [without Firefox]
Right now, we’re passing in a start position rather than a pure value (“56” for “45%”). That start position is based on an assumption that the overall scale is 100%
. In order to make this a more flexible tool, I thought it would be fun to contain all the math, including the scale, inside CSS. Here’s what it would look like:
<dl class="chart" style="--scale: 100">
<dt class="date">2000</dt>
<dd class="bar" style="--value: 45">45%</dd>
<dt class="date">2001</dt>
<dd class="bar" style="--value: 100">100%</dd>
<!-- etc… -->
</dl>
Then we can calculate the --start
value in CSS, before applying it.
.bar {
--start: calc(var(--scale) + 1 - var(--value));
grid-row-start: var(--start);
}
With both the overall scale and individual values in CSS, we can manipulate either one individually. Change the scale to 200%
, and watch the chart update accordingly:
See the Pen Bar Chart with Sale – no firefox by Miriam Suzanne (@mirisuzanne) on CodePen.
Both Chrome and Safari handle it beautifully, but Firefox seems unhappy about calc
values in grid-positioning. I imagine they’ll get it fixed eventually. For now, we’ll just have to leave some calculations out of our CSS.
Sad, but we’ll get used to it. 😉
There is much more we could do, providing fallbacks for older browsers – but I do think this is a viable option with potential to be accessible, semantic, performant, and beautiful. Thanks for starting that conversation, Robin!
If using
<meter>
, one wouldn’t have to code the lengths into the CSS:Meter is certainly a (underused) markup option, with or without my approach – one of many valid and semantic ways to style a bar graph. But this ignores everything else that we can do with the single variable in addition to setting lengths. This is not about the “correct” bar-graph approach, but allowing data to influence styles in a more direct and dynamic way.
What would you think of using rotated s inside of a table? I think that is more semantic, easier to implement, and has better support.
I know, the point was CSS. Asking purely for the sake of getting thoughts.
I’d love to see a demo to know what you mean by “using rotated s inside of a table”. How exactly do you see that being easier to implement? I think table would work great – even styled like I do in the article.
There are many ways to make a chart. As you note: I’m mostly interested in how variables allow more data-driven styling, with no bloat, and a single inline value that can be used dynamically by several different CSS properties. That flexibility doesn’t exist anywhere outside of Custom Properties.
This is among the most practical uses of CSS custom properties I’ve seen so far, and could be made to degrade gracefully without too much effort. Nicely done!
Nice tut about drawing charts using pure css.
You can add OPERA 47.0.263 to your list of browsers which handle successfully calc values.
Rgds
Chris2mop
Thanks, superb post!
As an aside, strictly from an a11y angle, I think definition lists doesn’t have any mappings to OS accessibility APIs, meaning screen reader users wouldn’t benefit from the implied relationships.