Create your own CSS Grid, Grid System

Jump straight to the tutorial

There are many ways to create what is known commonly as a grid system. Over the years some popular frameworks that also provided their own grid systems were:

  • 960 gs

  • Skeleton

  • Bootstrap

More recently though there's:

  • Material

  • Tailwind

The common things they share are: 12 columns, gutters, margins.

Why 12 columns?

Multiples of 4 work well as a general rule, and apply to more than grid systems. With 12 columns I can initially divide my grid like so: 12 / 1, 12 / 2, 12 / 3, 12 / 4. Then there are sub-divisions within, and many combinations possible, for example 4 + 8 will give me a typical web layout that has a main content area with an aside.

Web designers occasionally complain about 12 columns being over-used, but despite being a very common approach it works well, and covers the majority of cases for a typical website.

Sometimes a custom solution is the only thing that makes sense

Traditionally grid systems are meant to be designed to fit the type of content they hang together. I learned this fundamental by reading Grid Systems in Graphic Design by Josef Müller-Brockmann, his book is referenced many times. After studying, applying and experimenting with the principles; and applying them to real-life web projects, I know that's for good reason.

Grid systems from frameworks

Prior to the CSS grid spec that we all know and love (which was published in 2017) most grid systems were implemented using HTML markup, for instance take Bootstrap.

<div class="container text-center">
 <div class="row">
  <div class="col">
   1 of 2
  </div>
  <div class="col">
   2 of 2
  </div>
 </div>
 <div class="row">
  <div class="col">
   1 of 3
  </div>
  <div class="col">
   2 of 3
  </div>
  <div class="col">
   3 of 3
  </div>
 </div>
</div>
Example code from Bootstrap's layout section on their website

Tailwind also promote the use of markup to construct layouts using a predefined set of constraints, one of which (like Bootstrap) is 12 columns.

<div class="grid">
 <div class="grid col-span-12">
  <div class="col-span-6">
   1 of 2
  </div>
  <div class="col-span-6">
   2 of 2
  </div>
 </div>
 <div class="grid col-span-12">
  <div class="col-span-4">
   1 of 3
  </div>
  <div class="col-span-4">
   2 of 3
  </div>
  <div class="col-span-4">
   3 of 3
  </div>
 </div>
</div>
Reproduction of the Bootstrap example code (Fig.1) using Tailwind

Tailwind advocate using utility classes, which means writing the same classes in HTML more than once, which, without putting measures in-place, over the lifetime of a codebase can be a maintenance headache.

Thoughts on how to keep things DRY

Can you imagine having to recreate the same grid, button, alert or form input more than once, with states? The possibilities for bugs and inconsistency are multiplied with every instance.

In order to create consistency, reduce repetition and avoid a utility class soup, you'll want to combine Tailwind with templates, components or mixins, otherwise you and other developers are redefining the same things. This never works out unless you aren't concerned with maintenance, consistency, repetition and keeping people who have an eye for detail happy. This isn’t something Tailwind appears to teach in their docs.
In a future article I will demonstrate how to deal with this specific problem, if you want to find out sooner rather than later, let me know here.

Tutorial

Fluid 12 column grid

Creating a fluid 12 column grid, with equal columns and gutters, using CSS Grid is as straight forward as:

.grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: 1rem;
}
Initial code for a 12 fluid column grid using CSS Grid

Then I apply the .grid class to HTML

<div class="grid">
  <h1>Title</h1>
</div>

And finally, span all the columns:

h1 {
 grid-column: 1 / span 12
}

Note how simple that was, the result is also simple.

Bear in mind that due to the nature of CSS Grid being concerned about content first and foremost, and because the grid is divided with fr units, by placing content in any number of columns that can't contain it, the overall alignment of the grid is distorted.

The word astonishment is distorting the grid due to it being too wide
The word astonishment is distorting the grid due to it being too wide

Centralising and containing the grid

With a few adjustments to the previous code I can create a container, by default it will be central to the viewport. In order to create it I need to decide the width it will be, including the gutters on the outside (I usually refer to these as margins), and then divide by 2 and subtract that amount using calc() and vw units. calc() is a CSS superpower here as it can subtract one completely different type of unit from another, go calc()!

:root {
  --container-width: 768px;
  --fluid-margin: calc(50vw - var(--container-width) / 2)
}

.grid {
  display: grid;
  grid-template-columns: var(--fluid-margin) repeat(12, 1fr) var(--fluid-margin);
  column-gap: 1rem;
}

.header {
  grid-column: 2 / span 12;
}

.hero {
  max-width: 100%;
  height: auto;
  grid-column: 1 / span 14;
}

.content {
  grid-column: 2 / span 12;
}
How to centralise the container that now houses the columns, adding fluid space where there is excess

In order to see the content within the container I created I place it within the second and thirteenth columns. Due to the addition of the container my grid has an extra 2 columns, the starting column for content has to change, unless I want to add a full-bleed image (Fig. 8).

Centralised, contained grid with full-bleed image
Centralised, contained grid with full-bleed image

The example assumes that I only ever want to constrain the content within a 1024px width container, therefore will only ever be visible above a viewport width of 1024px.

There's a working CodePen of this example right here.

Change the number of columns across breakpoints

12 columns work well, but may not always be necessary on smaller screens with less real-estate. To change the amount of columns I can make some changes:

:root {
  --container-width: 1024px;
  --grid-columns-xs: 6;
  --grid-columns-lg: 12;
  --grid-columns: var(--grid-columns-xs);
  --fluid-margin: calc(50vw - var(--container-width) / 2)
}

@media(min-width: 768px) {
  :root {
    --grid-columns: var(--grid-columns-lg);
  }
}

.grid {
  display: grid;
  grid-template-columns: var(--fluid-margin) repeat(var(--grid-columns), 1fr) var(--fluid-margin);
  column-gap: 1rem;
}

.header {
  grid-column: 2 / span var(--grid-columns);
}

.hero {
  max-width: 100%;
  height: auto;
  grid-column: 1 / span calc(var(--grid-columns) + 2);
}

.content {
  grid-column: 2 / span var(--grid-columns);
}
Example demonstrating how calc() can be used to save repetition

Again, calc() is my friend here, however, what becomes apparent is the code is becoming harder to understand! Not only that but it doesn't look that extensible.

A less cumbersome way to do it, one of my preferences is to use named grid lines:

:root {
  --container-width: 1024px;
  --grid-columns-xs: 6;
  --grid-columns-lg: 12;
  --grid-columns: var(--grid-columns-xs);
  --fluid-margin: calc(50vw - var(--container-width) / 2)
}

@media(min-width: 768px) {
  :root {
    --grid-columns: var(--grid-columns-lg);
  }
}

.grid {
  display: grid;
  grid-template-columns: [fm-start] var(--fluid-margin) [c-start] repeat(var(--grid-columns), 1fr) [c-end] var(--fluid-margin) [fm-end];
  column-gap: 1rem;
}

.header {
  grid-column: c;
}

.hero {
  max-width: 100%;
  height: auto;
  grid-column: fm;
}

.content {
  grid-column: c;
}
How to use named grid lines to save repetition

To recap, I created named grid lines for fluid-margin (fm) and content (c), I used abbreviations for succinctness as that's sometimes how I like to roll, CSS Grid uses its superpowers here to do some heavy-lifting, the spec accepts named grid lines that start with -start and end with -end and does the rest.

Due to the combination of named grid lines, custom properties and @media(), all I need to change across breakpoints is a single custom property, the rest is taken care of by the way I have set up the surrounding code.

The code is now extensible, I can keep building on it adding more containers and breakpoints as needed without having to faff around too much with the code I have already written.

There's a working CodePen of this example right here.

If you liked this, feel free to subscribe so I can let you know when I have new content available. Feel free to share on Twitter or wherever you want.