Editors note: this post is just an experiment to play with new CSS properties and so the code below shouldn’t be used without serious improvements to accessibility.
I have a peculiar obsession with charts and for some reason, I want to figure out all the ways to make them with CSS. I guess that’s for two reasons. First, I think it’s interesting that there are a million different ways to style charts and data on the web. Second, it’s great for learning about new and unfamiliar technologies. In this case: CSS Grid!
So this chart obsession of mine got me thinking: how would you go about making a plain ol’ responsive bar chart with CSS Grid, like this:
Let’s take a look at how I got there!
The fast and easy approach
Since Grid can be confusing and weird at first glance, let’s focus on making a really hacky prototype to begin with. To get started we need to write the markup for our chart:
<div class="chart">
<div class="bar-1"></div>
<div class="bar-2"></div>
<div class="bar-3"></div>
<div class="bar-4"></div>
<!-- all the way up to bar-12 -->
</div>
Each of those bar-
classes will make up one whole bar in our chart and, as yucky as this might seem, for now we won’t worry too much about semantics or labelling the grid or the data. That’ll come later – let’s focus on the CSS so we can learn more about Grid.
Okay so with that we can now get styling! We need 12 bars in our chart with a 5px gap between them so we can set our parent class .chart
with the relevant CSS Grid properties:
.chart {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 5px;
}
That’s pretty straight forward if you’re at all familiar with Grid but what it effectively describes is this: “I want 12 columns with each of the child elements having an equal width (1 fraction) with a 5px gap between them”.
But now, here’s the sneaky part: with Grid we can use the grid-template-rows
property to set the height of each our chart’s bars:
.chart {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: repeat(100, 1fr);
grid-column-gap: 5px;
}
We can use that neat new property to make 100 rows in our grid and this way we can then set each of our bars to be a percentage of that height and it’ll make the math easy for us. Again, we’re using that repeat()
function so that each of our rows make up the same height.
Before I explain that all in more detail, let’s give our chart a max-width and set it to the center of the screen with flex:
* { box-sizing: border-box; }
html, body {
margin: 0;
background-color: #eee;
display: flex;
justify-content: center;
}
.chart {
height: 100vh;
width: 70vw;
/* other chart styles go here */
}
At this point our chart will still be empty because we haven’t told our child elements to take up any space in the grid. So let’s fix that! We’re going to select every class that contains bar
and use the grid-row-start
and grid-row-end
properties to make them fill up the vertical space in our grid and so eventually we’ll end up changing one of these properties to define the custom height of each bar:
[class*="bar"] {
grid-row-start: 1;
grid-row-end: 101;
border-radius: 5px 5px 0 0;
background-color: #ff4136;
}
So if you’re bewildered by those grid-row
properties then that’s okay! We’re telling each of our bars to start at the very top of the grid (1) and then end at the very bottom (101). But why are we using 101 as a value for that property when we only told our grid to contain 100 rows? Let’s explore that a little bit before we move on!
Grid lines
One of the peculiar things about Grid that I hadn’t considered before working on this demo was the concept of grid lines which is super important to understanding this new layout tool. Here’s an illustration of how grid lines are plotted in a four column, four row grid:
This new example contains four columns and four rows with the following styles:
.grid {
grid-gap: 5px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.special-col {
grid-row: 2 / 4;
background-color: #222;
}
grid-row
is a shorthand property for grid-row-start
and grid-row-end
with the first value where we want the element to start on the grid and the final value where we want it to end. But! This means we want that special element here to start at grid line 2 and end at grid line 4 – not at the end of row 4. If we wanted that black box to fill all 4 rows then we’d need it to end at line 5 or grid-row: 2 / 5
which makes an awful lot of sense if you think about it.
In other words, we shouldn’t think of elements taking up whole rows or columns in a grid but rather only spanning between these grid lines. That took me a while to conceptually figure out and get used to after I dived into a recent tutorial by Jen Simmons on the matter.
Anyway!
Back to the demo
So that’s the reason why in our chart demo we end all columns at line 101 and not 100 – because we want to it fill up the last row (100) so we have to send it to that particular grid line (101).
Now, since our .chart
class uses vw/vh units, we also have a nicely responsive chart without having to do much work. If you resize that graph below you’ll find it nicely packs down or stretches to always take up the whole viewport:
From here we can begin to style each of the individual bars to give them the right data, and there are a whole bunch of different ways we can do this. Let’s take a look at just one of them.
First, let’s imagine we want the first bar in our chart, .bar-1
, to be 50/100 or half the height of the chart. We could write the following CSS and be done with it:
[class*="bar"] {
grid-row-end: 101;
}
.bar-1 {
grid-row-start: 50;
}
That looks fine! But, here’s the catch – what we’re really declaring with that grid-row-start
is for the bar to start at “50” and end at “101” but that’s not really what we want. Here’s an example: let’s say the data changes in this hypothetical example and we need it to now be 20/100. Let’s go back and change the value:
.bar-1 {
grid-row-start: 20;
}
That’s not right! We want the bar not to start in the grid at 30 but be 30% the height of the chart height. We could change our value to grid-row-start: 20;
or we could use the grid-row-end
property instead, right? Well, not quite:
.bar-1 {
grid-row-end: 20;
}
The size of the bar is correct but the position is wrong because we’re telling the bar to end at 30/100. So how do we fix this and make our code super easy to read? Well, one approach is to take use Sass to do the math for us. Ideally we’d like to write something like the following:
.bar-1 {
// makes a bar that's 60/100 and positioned at the bottom of our chart
@include chartValue(60);
}
And no matter what value we put into that mixin we always want to get the correct height and position of the chart on the grid. The math that powers this mixin is actually pretty darn simple: all we need to do is take our value, deduct it from the total number of rows and then attach it to the grid-row-start
property, like this:
@mixin chartValue($data) {
$totalRows: 101;
$result: $totalRows - $data;
grid-row-start: $result;
}
.bar-1 {
@include chartValue(20);
}
So the final value that gets churned out by our Sass mixin is grid-row-start: 81
but our code is super legible! We don’t even have to look at our grid to know what’s going to happen – the chart item will be positioned at the bottom of the grid and the value will always be correct.
How do we create all those grid classes though? I think one neat approach is to just let Sass generate all those classes automatically for us. With just a little modification to our code we could do something like this:
$totalRows: 101;
@mixin chartValue($data) {
$result: $totalRows - $data;
grid-row-start: $result;
}
@for $i from 1 through $totalRows {
.bar-#{$i} {
@include chartValue($i);
}
}
This will iterate over all of the rows in our chart and generate an individual class for that row size. And so now we could update our markup like so:
<div class="bar-45"></div>
<div class="bar-100"></div>
<div class="bar-63"></div>
<div class="bar-11"></div>
And there we have it! We don’t have to write individual classes for each of our elements by hand and we can easily update our chart by just changing the markup. This Sass loop will spit out a lot of classes that go unused but there’s plenty of tools out there to strip those out.
One last thing we can do with our grid is style each column with a color by odd/even:
[class*="bar"]:nth-child(odd) {
background-color: #ff4136;
}
[class*="bar"]:nth-child(even) {
background-color: #0074d9;
}
And there we have it! A responsive chart built with CSS Grid. There’s plenty that we could do to tidy up this code, however. The first thing we should probably do is make sure that we’re using semantic markup and use a tool to remove all those classes that are being spat out by our Sass loop. We could also dig into how this chart is rendered on mobile and think about how we ought to label each column and chart axis.
But for now, this is just the beginning. The TL;DR of this post: CSS Grid can be used for all sorts of things rather than just setting text and images next to each other. It opens up a whole new branch of web design for us to experiment with.
How about custom data attributes and the use of attr() to set the heights?
No browser support.
I’ve had that idea before… unfortunately it doesn’t seem to be widely supported. The use of attr() seems to work best in the content property of pseudo elements and that’s about it. :/
Neat!
Personally, I don’t really like putting data in my classnames, but that’s an easy fix…
Instead of
it’d be pretty easy to just do
and tweak the selectors accordingly.
Hi Robin, do you see this method as just an interesting experiment with css grid, or do you think it offers legitimate advantages over using flexbox for css-based bar charts?
For reference here is how I would do it with flexbox:
I liked your approach. Thanks for sharing.
Similar results without using grid … here’s what I came up with, just for fun:
Not recommended for actual use … the spacing between bars isn’t always consistent, for one thing
I did something similar awhile back with divs. Could be simplified, and impproved, but Grid seems like overkill for this sort of thing.
https://codepen.io/Jpburns/full/mApbYk/
As Hugo Giraudel points out on Twitter, this is a neat visual effect, but as-is, the “chart” is completely inaccessible. The data is exposed only through the CSS styles, nothing else.
A few people have replied that SVG would be better. I disagree. There is nothing automatically semantic about SVG. A
<rect>
has no more meaning than a<div>
.To give the data in this chart semantic meaning you need:
to clearly indicate that you have a set of distinct data values
to communicate what the numerical value is, as part of the accessible name of each item in that set.
There are ways to add that information to SVG. But you can also add it to HTML elements.
For the first part (clarifying that this is a list of items), you can do that simply by using the correct semantic HTML elements for a list of items (
<ol>
and<li>
). For the second part, you either need to include the data as the text content of the HTML element or use ARIA to add a label.Here’s my approach:
I changed it from Robin’s final code, to use semantic HTML (a list of items), to separate the data value from the class, and to add visible and screen-reader accessible labels for the values. This also means that the data is visible even if grid isn’t supported.
Of course, you still can’t copy & paste this chart into a web site, because it still has no labels or context explaining what the data is. But that’s a separate matter. The key change is that everyone will now get the same amount of (context-free) information.
The accessibility is also still not ideal. I don’t really like using
aria-label
for the raw numbers. I originally used adata-*
attribute. But many screen readers don’t read the CSS generated-content labels. Usingaria-label
attribute makes it more likely that they still read the values as the name of each list item.But for a real bar chart, you’d probably need the label to include the x-axis value, as well. Which then makes it difficult to use the
aria-label
attribute in CSS selectors and generated content.Basically, in any real-world situation, you’d need to duplicate the values: once for the accessible label, formatted for human beings, and once in an attribute or custom property that you use to define the style and sizing.
A real accessible approach indeed I think.
Taking from there, I’ll add my touch: why not put the actual content (the value) as a content? But then what’s the use of the label? the meaning of the value of course!
The css is basically the same, just adding content in the HTML and ::before and ::after classes so to display the data.
This is a great note, Amelia! We also have an upcoming post that goes into a lot more of the accessibility concerns that you have and develops on my ideas considerably.
Awesome!
You can also use a negative value with
grid-row-end
; basically saying “1” from the end.Awesome post!
Something is bothering me tho, why is there a difference between
grid-row-end: 101;
andgrid-row-end: 102;
with these row definitionsgrid-template-rows: repeat(100, 1fr);
?I just came back here to ask the same question! Didn’t understand why there was a 1px or so gap at the bottom of bars when I used grid-row-end: 101 (noticed it when I added a border to chart). Replaced 101 with 102 to fix it, but still it’s bothering me that why 101 didn’t work.
Hi Robin. Thanks for the detailed explanation. As I just started learning CSS Grid, I thought I wouldn’t be able to understand it, but wrote really well that even a newbie like me was able to understand it.
What would be cool is if you could simply reverse the order of the row’s grid-auto-flow so they start at the bottom… much like flex-direction can be reversed.
Actually come to think of it… a dirty way to achieve that would be to simply transform: scaleY(-1) … but that could become problematic if you need to add other elements like text to the same container.
You could get accessibility benefits and auto-sizing according to data simultaneously by using
<meter>
, rotated sideways, I think. It even takes a<label>
!With CSS Grid you can use negative values to position your boxes from bottom to top instead. Which is a nice things to use instead of calculating the value with SASS.
What about continuous values? 0.9%,0.99%, 0.999%, 1.3434%, 2.54354% and so on? Create css-selectors like a
.bar-0.9854"
?