Making A Bar Chart with CSS Grid

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:

See the Pen CSS Grid Chart Final by Robin Rendle (@robinrendle) on CodePen.

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;
}

See the Pen CSS Grid Chart 1 by Robin Rendle (@robinrendle) on CodePen.

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:

See the Pen CSS Grid Chart 1 by Robin Rendle (@robinrendle) on CodePen.

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;
}

See the Pen CSS Grid Chart 2 by Robin Rendle (@robinrendle) on CodePen.

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;
}

See the Pen CSS Grid Chart 3 by Robin Rendle (@robinrendle) on CodePen.

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;
}

See the Pen CSS Grid Chart 4 by Robin Rendle (@robinrendle) on CodePen.

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);
}

See the Pen CSS Grid Chart 5 by Robin Rendle (@robinrendle) on CodePen.

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>

See the Pen CSS Grid Chart 6 by Robin Rendle (@robinrendle) on CodePen.

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;
}

See the Pen CSS Grid Chart 7 by Robin Rendle (@robinrendle) on CodePen.

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.