Recreating the GitHub Contribution Graph with CSS Grid Layout

While learning CSS Grid Layout, I’ve found that the best way to internalise all the new concepts and terminology is by working on various layouts using them. Recently, I decided to try to recreate the GitHub Contribution graph using CSS Grid Layout, and found it was an interesting challenge.

Contribution Graph

As I always find while working with CSS Grid Layout, I end up with far less CSS than I would have using almost any other method. In this case, the layout-related part of my CSS ended up being less than 30 lines, with only 15 declarations!

To explain how I recreated this layout, let’s break it down into four stages:

  1. The overall graph grid
  2. The "days" column
  3. The "months" row
  4. The graph squares

The Overall Graph Grid

The broad layout of the contribution graph is broken up into three main areas - the days column, the months row, and the individual squares.

Within the main .grid element, we have three elements:

<div class="grid">
	<ul class="months"></ul>
	<ul class="days"></ul>
	<ul class="squares"></ul>
</div>

I laid out the three areas within the main graph using the grid-template-areas property. This property allows us to, in a more explicit way, define where the different areas of a grid fall. To do this, we first name each area of the grid.

.months { grid-area: months; }
.days { grid-area: days; }
.squares { grid-area: squares; }

Then, using these names, we write out how we want the areas to be laid out. The grid-template-areas property accepts space-separated strings. Each string represents a row, and within each string we have columns.

.graph {
  display: inline-grid;
  grid-template-areas: "empty months"
                       "days squares";
}

I prefer to separate each string by a new line, so it is more visually apparent that each string represents a row, but a space is all that is needed. The above property could just as easily be written in the following way -

.graph {
  grid-template-areas: "empty months" "days squares";
}

You'll notice I added an unnammed grid area, empty to the grid-template-areas value. This is because we want the months area to start from the second column and the days area to start from the second row. If we use a grid area name that doesn't correspond to an actual grid area, we can essentially create a "phantom" element. We can act as if there is an element there without having anything obstructing our actual markup. Cool, right?!

In addition to laying out these grid areas, there are two more styles that need to be applied to graph. First, we want to add some spacing between each element, which we add with the grid-gap property.

.graph {
  grid-gap: 10px;
}

Finally, we want the the first column to only take up as much space as it needed by the content within it, but the second column to fill up the rest of the available space. We can achieve the latter using the fr unit, which represents the remaining available space within a grid.

.graph {
  grid-template-columns: auto 1fr;
}

And that’s all the styles we need to apply to the containing graph element.

The "Days" Column

The list of days on the left side of the contribution graph is a simple one-column grid with exactly 7 rows.

This layout is achieved with exactly 2 lines of CSS -

:root {
  --square-size: 15px;
}

.days {
  display: grid;
  grid-template-rows: repeat(7, var(--square-size));
}

We use the repeat() function here, passing in the following options:

  1. For how many rows we want the sizing (see the next item) to apply for. In this case, we chose 7 because there are 7 days in the week.
  2. The size we want each row to be. In this case, we used the same size the individual squares are going to be, using the --square-size variable declared earlier in the stylesheet

Lastly, we want some spacing between each of the rows. As with the spacing between the areas in the .graph element, we use the grid-gap property, passing in the amount of spacing we want.

:root {
  --square-size: 15px;
  --square-gap: 5px;
}

.days {
  grid-gap: var(--square-gap);
}

The "Months" Row

The list of months at the top of the contribution graph is a one-row grid.

However, unlike the list of days, the amount of space (i.e. columns) taken up by each items is not so straightforward. Ideally, we would want each month to take up the equivalent of 4 columns to represent four weeks. We could achieve this using a similar method to how we laid out the weeks.

:root {
  --square-size: 15px;
  --square-gap: 5px;
  --week-width: calc(var(--square-size) + var(--square-gap));
}

.months {
  display: grid;
  grid-template-columns: repeat(12, calc(var(--week-width) * 4));
}

However, each month doesn’t span across exactly 4 even weeks. There is some overlap, with some months spanning across less or more. Because of this, I opted to manually set the column width for each month.

.months {
  display: grid;
  grid-template-columns: calc(var(--week-width) * 4) /* Jan */
						 calc(var(--week-width) * 4) /* Feb */
						 calc(var(--week-width) * 4) /* Mar */
						 calc(var(--week-width) * 5) /* Apr */
						 calc(var(--week-width) * 4) /* May */
						 calc(var(--week-width) * 4) /* Jun */
						 calc(var(--week-width) * 5) /* Jul */
						 calc(var(--week-width) * 4) /* Aug */
						 calc(var(--week-width) * 4) /* Sep */
						 calc(var(--week-width) * 5) /* Oct */
						 calc(var(--week-width) * 4) /* Nov */
						 calc(var(--week-width) * 5) /* Dec */;
}

Since I wasn’t relying on real data, I just copied what I saw on my actual GitHub graph at the time. Ideally, we would have a way to specify the widths of each column more programatically, but this is the way I decided to implement for the demonstrative purpose.

The Individual Squares

The styles for the individual squares are mostly shared with the .days column, since the spacing and number of rows are meant to be identical.

.days,
.squares {
  display: grid;
  grid-gap: var(--square-gap);
  grid-template-rows: repeat(7, var(--square-size));
}

In addition to those shared styles, the individual squares are different in a number of ways.

The first is the direction in which the squares are laid out. By default, items within a grid are laid out in the direction of the English language, i.e. left-to-right then top-to-bottom. However, the squares in the GitHub contribution graph are laid out differently, top-to-bottom then left-to-right.

To lay our squares out in this way, we use the grid-auto-flow property.

.squares {
  grid-auto-flow: column;
}

Lastly, we want each column/square to be a set width (var(--square-size)). However, unlike the rows, we do not know or want to specify the exact amount of columns we have. So, instead of using the grid-template-columns property, we can use the grid-auto-columns property, setting it to the width we want each column to be, and letting the grid figure out how many columns are eventually drawn.

.squares {
  grid-auto-columns: var(--square-size);
}

The Finished Product

You can see how everything works together in the CodePen below.

See the Pen GitHub Contribution Graph in CSS Grid Layout by Ire Aderinokun (@ire) on CodePen.

blog comments powered by Disqus