Scalable, Maintainable Grids with CSS Variables

Part 2 of our in-depth look at CSS Grid

Having used CSS Grid for quite a while now I can safely say it’s a superior layout method to anything we’ve had before. But, as with anything in CSS, it’s not without its own set of issues. In this article I want to share my methodology for working with CSS Grid, and how CSS Variables can help keep our coding style consistent, DRY and maintainable.

The main issue I’ve encountered with CSS Grid workflow is keeping a consistent style to your code that anyone on the team who picks up the project can easily follow. With Grid, there are several methods for defining your grid and placing child items, and multiple units or auto properties you can use to define your grid tracks (rows and columns) and gutters.

Placing items can use any of the following:

  • Grid line numbers
  • Named grid lines
  • span $number for the number of tracks a grid item should span
  • A combination of all of the above in grid-column-start, grid-row-start, `grid-column-end`, `grid-row-end` or the shorthand `grid-row` and `grid-column` properties
  • `grid-template-areas` (on your parent grid container)
  • Auto-placement (with various options for controlling flow)

Your stylesheet can easily become bloated and confusing, unless you invest time in a systematic approach. After experimenting with CSS Grid over the course of a few projects, I’ve found an approach that works well for me.

In the vast majority of cases, when building pages we need to define our grid columns; our grid rows are generally (with a few exceptions, mainly at component-level) best left as implicit tracks. For this reason, I like to create a class that when applied to any element causes that element to become a grid container. I keep the properties to a bare minimum, to allow maximum versatility within the strict parameters of our grid, for example:

.grid {
display: grid;
grid-template-columns: 20px repeat(12, 1fr) 20px;
}

We also need to add fallbacks for browsers that don’t support grid. We can do this by setting our grid container to `display: flex` and wrapping our grid properties inside a feature query. We’ll probably need to do some work at a later date to get our layout looking good on IE11 and below – for this my preferred approach is to use `@supports` on each of the grid cells and declare widths as and when needed.

.grid {
display: flex;
flex-wrap: wrap;
padding: 0 20px;
margin: 0 auto;
max-width: 1200px;

@supports (display: grid) {
display: grid;
grid-template-columns: 20px repeat(12, 1fr) 20px;
max-width: none;
padding: 0;
}
}

This means that anything we put inside the grid container will fit in a max-width wrapper, even when CSS Grid isn’t supported in the browser. (For simplicity I’m leaving out media queries here, but of course you might only want to initialise your grid after a specified breakpoint.) Now I can wrap any component in this grid class. Then I add my grid cells, which must be direct children of the element that has the grid class. In most cases, for clarity, I give these a grid class, so that I can keep the layout in its own SCSS file, e.g:

.grid__cell--full-width {
grid-column: 1 / span 14; // starts at grid-line 1, spans the full width of the outer grid
}

.grid__cell--wrapper {
grid-column: 2 / span 12; // starts at grid-line 2, spans the full width of the wrapper
}

.grid__cell--article {
grid-column: 2 / span 8;
}

My preference is to try and keep grid styling separate to component styling – unless you’re using Grid inside a component. Even then it can be helpful to use a Grid class – but every case is different.

While we don’t (yet) have subgrid in the CSS Grid specification, we can nest other grids inside grid items. Grid items can also be grid containers, so we can do this if needed:

.grid__cell--wrapper {
grid-column: 2 / span 12;
display: grid;
grid-template-columns: repeat(12, 1fr); // this defines our subgrid
}

Recently I’ve been tasked with building quite a lot of 24-column grids. Placing items on a large grid can get pretty messy, so I find the most helpful approach is to name my grid lines. I don’t name all of them, but it’s useful to name the ones that items most often need to be aligned with. Otherwise it’s easy to lose track of how many columns an item is supposed to span, and if you inadvertently place items outside of your explicit grid then implicit tracks will be created, which make your whole layout go crazy (unless this is the desired behaviour, of course!).

.grid {
display: flex;
//...

@supports (display: grid) {
display: grid;
grid-template-columns: [outer-start] 20px [wrapper-start] repeat(12, 1fr) [wrapper-end] 20px [outer-end];
//...
}

@media (min-width: 1400px) {
grid-template-columns: [outer-start] 1fr [wrapper-start] repeat(12, $max-col) [wrapper-end] 1fr [outer-end];
}
}

.grid__cell--wrapper {
grid-column: wrapper-start / wrapper-end; // a simpler way of placing a 12-column grid cell
}

.grid__cell--article {
grid-column: wrapper-start / span 8;
}

Now I’ve added a breakpoint, where we’ve changed our `grid-template-rows` property, using named grid lines means we don’t need to update our grid cells in this case. However, our code is starting to get a little more complex. This is where CSS Variables can help.

CSS Variables work a bit like preprocessor variables, in that you define values to reuse throughout your code. But unlike preprocessor variables they are dynamic, meaning they can be updated at different breakpoints, in different components and even with Javascript. So one variable could potentially have multiple values within your stylesheet. (For this reason, you need to be careful of context, and only redefine your variables where you need to – otherwise you might get unexpected results!)

You can define CSS Variable in the root or inside components. In the code below I’m updating the variable at 1400px breakpoint and again at the 1600px breakpoint:

:root {
--columnWidth: 1fr;
--padding: 20px;

@media (min-width: 1400px) {
--columnWidth: 70px;
--padding: 1fr;
}

@media (min-width: 1600px) {
--columnWidth: 100px;
}
}

Then we can use it in our component:

.grid {
display: flex;
//...

@supports (display: grid) {
display: grid;
grid-template-columns: [outer-start] var(--padding) [wrapper-start] repeat(12, var(--col)) [wrapper-end] var(--padding) [outer-end];
//...
}
}

Now we don’t even need a media query for our grid class – our code becomes instantly simpler and more maintainable. Here’s a working example:

<p data-height="392" data-theme-id="0" data-slug-hash="YLKLrL" data-default-tab="css,result" data-user="michellebarker" data-embed-version="2" data-pen-title="CSS Grid + Variables - simple layout" class="codepen">See the Pen <a href="">CSS Grid + Variables - simple layout</a> by Michelle Barker (<a href="">@michellebarker</a>) on <a href="">CodePen</a>.</p>
<script async src="
"></script>

And here’s a more complex example, where we’re using a 24-column grid (we need more media queries for this one!):

<p data-height="345" data-theme-id="0" data-slug-hash="MGgvyY" data-default-tab="css,result" data-user="michellebarker" data-embed-version="2" data-pen-title="CSS Grid + Variables" class="codepen">See the Pen <a href="">CSS Grid + Variables</a> by Michelle Barker (<a href="">@michellebarker</a>) on <a href="">CodePen</a>.</p>
<script async src="
"></script>