Lawrence Eagles Senior full-stack developer, writer, and instructor.

How to build a custom grid with SASS

9 min read 2624

CSS Grid Logo

Introduction

A grid or grid system is a collection of vertical and horizontal lines that defines a structure to stack design elements in a consistent and manageable fashion.

Grids are project-agnostic, meaning they can be used on any project. A grid typically consists of rows, columns, and gutters (the space between the rows and columns). And they enable design elements to be stacked vertically or horizontally.

Web technologies such as HTML and CSS have existed for years without a grid system. However, CSS frameworks such as Bootstrap have popularized the use of grid systems on the web.

Also, W3C has released the CSS Grid module 1 to define two-dimensional grid-based layouts. CSS grid or CSS grid layout is currently not an official standard, it’s a W3C candidate recommendation but it has been supported by most major browsers.

What you will learn

In this article you would learn about the following:

  • Grid systems
  • How to build a custom grid
  • How to setup your CSS workflow to use responsive units (em)
  • How to use the CSS calc() function
  • How to use the :not() pseudo-selector and the last-child pseudo-class

Why grid systems?

Grid systems make it very easy to create responsive web designs, mobile-friendly designs and are just well suited for almost any kind of layout. They also give an easy and predictable approach to scaffolding layout designs which can help improve productivity.

What we will be building

There are several ways to build a custom grid in CSS. We can use the plain, old CSS float property, flexbox, or CSS grid. In this article, we will be using float because it is very straightforward and supported by all browsers. Below is an image of the grid we will be building in this section:

Custom Grid Example

Project setup

We will build our grid with HTML 5 and SASS (which compiles to pure CSS). We are using SASS (SCSS) because it adds a lot of power and elegance to CSS. SASS features such as nesting and the parent selector are used extensively in this project. Together they make our code clean and maintainable.

I have set up a starter project to get us started easily. Get the starter files by cloning this repository. Then follow the steps below to start the dev server:

We made a custom demo for .
No really. Click here to check it out.

  • cd into the project directory
  • install dependencies by running:
    #NPM
    npm install
    
    #YARN
    yarn install
  • Start the dev server by running:
    #NPM
    npm start
    
    #YARN
    yarn start

You can now view your app at 127.0.0.1:8080

  • We still need to watch our SASS file and compile it. To do this open a new terminal tab and run:
    #NPM
    npm run sass
    
    #Yarn
    yarn run sass

This starts watching our SCSS file and compiles it into our style.css file. Our project setup is now complete. We can start building our grid in the next section.

Building a custom grid

The markup for our grid is already provided in the starter files. In this section we will focus on the styling our grid by following the steps below:

Basic reset using the universal selector *

The universal selector (*) applies the declared styles to all HTML elements in the page:

*{
  margin: 0px;
  padding: 0px;
  box-sizing: border-box;
}

In the code above we have reset our margin and padding to 0px. This is because browsers, by default, add some margin or padding to certain elements (e.g h1 elements). We do not want this since it can complicate our calculation.

The second (box-sizing) rule is more interesting. This CSS property allows us to change the behavior of the CSS box model.

Consequently, we change how the total width and height of an element are calculated.

The value of the box-sizing property can be used to adjust the CSS box model behavior. By default, the value is set to content-box.

This means that the width of any border or padding of an element is added to the final width of that element. And this makes calculating the final width unclear.

By setting the box-sizing property to border-box, the browser does not add the padding and border of an element to its total width. So, it is easy to calculate an element’s width.

Set up a workflow to use responsive unit (rem)

This pattern gives us an easy way to change all the measurements on our page with one simple setting. It involves setting our root font-size, for easy calculation using rem like this:

html {
  font-size: 10px;
}

Since 1rem is equal to the root font-size. 1rem in our case is now 10px. This will make our calculation easier going forward. Since we know that 2rem should be 20px, 1.6rem should be 16px, etc.

Also, we can now change all the measurements on our page by changing the root font-size specified above.

This is very important. For example, when we hit a breakpoint to display our page on a mobile device. We will need a way to decrease all the measurements on our page at the same time.

With our current pattern, we do not have to write complex media queries. We handle this by changing the global font-size.

Next, we will save our static values to SCSS variables like this:

// GRID
$grid-width: 100rem; // 1000px
$gutter-vertical: 4rem; //40px
$gutter-horizontal: 4rem; // 40px

Add the grid markup:

<section class="grid-test">
   <div class="row">
       <div class="col-1-of-2">
           Col 1 of 2
       </div>
       <div class="col-1-of-2">
           Col 1 of 2
       </div>
   </div>
   <div class="row">
       <div class="col-1-of-3">
           Col 1 of 3
       </div>
       <div class="col-1-of-3">
           Col 1 of 3
       </div>
       <div class="col-1-of-3">
           Col 1 of 3
       </div>
   </div>
   <div class="row">
       <div class="col-1-of-3">
           Col 1 of 3
       </div>
       <div class="col-2-of-3">
           Col 2 of 3
       </div>
   </div>
   <div class="row">
       <div class="col-1-of-4">
           Col 1 of 4
       </div>
       <div class="col-1-of-4">
           Col 1 of 4
       </div>
       <div class="col-1-of-4">
           Col 1 of 4
       </div>
       <div class="col-1-of-4">
           Col 1 of 4
       </div>
   </div>
   <div class="row">
       <div class="col-1-of-4">
           Col 1 of 4
       </div>
       <div class="col-1-of-4">
           Col 1 of 4
       </div>
       <div class="col-2-of-4">
           Col 2 of 4
       </div>
   </div>
   <div class="row">
       <div class="col-1-of-4">
           Col 1 of 4
       </div>
       <div class="col-3-of-4">
           Col 3 of 4
       </div>
   </div>
</section>

Our markup above consists of six rows. Each has a varying number of columns. Each div that is a row has a class named row while each div that is a column has a class with a name beginning with col-. The column class is named depending on how many columns are in the row.

Kindly consider the code below:

<div class="row">
  <div class="col-1-of-2">
      Col 1 of 2
  </div>
  <div class="col-1-of-2">
      Col 1 of 2
  </div>
</div>

The code above is for a row with two equal columns. So each column has a class named col-1-of-2. This naming convention is maintained throughout the markup. This pattern enables us to target a wide range of elements for styling as we will see in the next section.

Style the rows and columns

.row {
    max-width: $grid-width;
    margin: 0 auto;

    [class^="col-"] {
        float: left;
        background-color: orange;
        padding: 1rem;
    }
}

The code above gives a max-width to all rows. A max-width is used because it performs better on devices with a small view-port.

Block-level elements (like our rows) always stretch and take up the full width of their container. By setting a width, we will prevent this behavior. But there will be a problem if the browser’s window is smaller than the element’s width. In this case, browsers will add a horizontal scrollbar to the page.

To prevent this, the max-width is used instead of the width property. This will cause the elements to take 100% of the viewport if the browser’s window is smaller than the specified max-width. So this performs better in smaller devices.

The margin: 0 auto; rule centralizes a block element inside of another block element. So, our grid is aligned center.

We styled the column by using a complex attribute selector [class^="col-"]. Here we reap the benefit of our column class naming pattern mentioned above. The attribute selector [class^="col-"] targets all the elements with a class name that starts with col- and applies the declared styles to them. So the CSS rules above, float all of our columns left, set the background-color to orange, and apply a padding of 1rem (10px).

There are other matching patterns for the attribute selectors. For instance:

  • [class$="col-"] would select any element with a class name that ends with "col-"
  • [class*="col-"] would select any element with a class name that contains "col-"

You can get more on this here.

When we float all the child elements (columns) in a parent element (rows) the parent elements collapses. Consequently, the height of the parent element is 0. To fix this we use the clearfix hack. The clearfix hack appends a pseudo element after an element to clear the float like this:

.row::after {
    content: "";
    display: table;
    clear: both;
}

We can add this to our code using SCSS nesting and & selector like this:

.row {
    max-width: $grid-width;
    margin: 0 auto;
    &::after {
        content: "";
        display: table;
        clear: both;
    }
    [class^="col-"] {
        float: left;
        background-color: orange;
        padding: 1rem;
    }
}

In SCSS the & selector refers to the parent element in this case the (row).

Create vertical and horizontal gutters

We have already declared the variables that store the values of the vertical and horizontal gutters in step two like this:

$gutter-vertical: 4rem;
$gutter-horizontal: 4rem;

Here we will create the vertical and horizontal gutters using these variables.

To create the vertical gutter, we add a bottom margin to all the rows except the last row.
And to create the horizontal gutter we add a right margin to all the columns inside a row, except the last column.

This is not straightforward because in both cases we do not want the CSS rule to apply to the last element. To do this we will use the :not() pseudo-selector and the last-child pseudo-class.

These enable us to selectively apply our CSS rule like this:

.row {
    max-width: $grid-width;
    margin: 0 auto;
    &:not(:last-child) {
        margin-bottom: $gutter-vertical;
    }

    &::after {
        content: "";
        display: table;
        clear: both;
    }

    [class^="col-"] {
        float: left;
        background-color: orange;
        padding: 1rem;
        &:not(:last-child) {
            margin-right: $gutter-horizontal;
        }
    }
}

The last-child pseudo-class selects the last element in a list of sibling elements. So when used with the :not() pseudo-selector the :not() pseudo-selector now selects every element except the last-child in a list of sibling elements. So our gutter is applied to every sibling element except the last-child element in the list.

Calculate the width of the grid columns

This is the tricky calculation for which we will use the extremely powerful CSS calc() function. The calc() function enables us to perform calculations in CSS when specifying CSS properties. One great advantage of the calc() function is that it allows us to mix units when performing mathematical operations like this:

div {
  width: calc(60% - 50px);
  height: calc(100rem / 20%);
}

p {
  font-size: calc(1.5rem + 1vh);
}

In our grid, we have six rows and each has a varying number of columns so we will take the calculations in succession.

Calculate the width of the col-1-of-2 columns

Row Width Col 1 of 2

The image above illustrates the first row. It has a max-width of 100 rem. And the gutter between both (col 1 of 2) columns is 4rem. To get the width of each of these columns, we subtract the horizontal gutter ($gutter-horizontal) from 100% of the width of the row. This gives us the width of both (col 1 of 2) columns then we divide our result by two to get the width of each (col 1 of 2) column like this:

.col-1-of-2 {
  width: calc((100% - $gutter-horizontal) / 2);
}

Calculate the width of the col-1-of-3 columns

Row Width Col 1 of 3

This is similar to our previous calculation. But from the image above we have two horizontal gutters and this needs to be factored into our calculation.

Subtracting both horizontal gutters (2 * $gutter-horizontal) from the 100% of the width of the row will give us the width of all three columns. Then dividing this result by three will give us the width of each col 1 of 3 column:

.col-1-of-3 {
  width: calc((100% - 2 * $gutter-horizontal) / 3);
}

Calculate the width of the col-1-of-4 columns

Row Width Col 1 of 4

As seen in the image above, this is also similar to our first two calculations. However, we have three horizontal gutters in this case.

By subtracting these horizontal gutters (3 * $gutter-horizontal) from 100% of the width of the row, we get the width of all four col-1-of-4 columns. Then we divide our result by four to get the width of each col 1 of 4 columns:

.col-1-of-4 {
  width: calc((100% - 3 * $gutter-horizontal) / 4);
}

Calculate the width of the col-2-of-3 column

Row Width Col 2 of 3

From our image above, the width of the col-2-of-3 column is the width of two col-1-of-3 columns and the horizontal gutter. Since we know that this gives us the width of one col-1-of-3 column:

calc((100% - 2 * $gutter-horizontal) / 3)

We simply multiply this by two and add a horizontal gutter to our result to get the width of the col-2-of-3 column as seen below:

.col-2-of-3 {
  width: calc(2 * ((100% - 2 * $gutter-horizontal) / 3) + $gutter-horizontal);
}

Calculate the width of the col-2-of-4 column

Row Width Col 2 of 4

From our image above we can see that the width of the col-2-of-4 columns is the width of two col-1-of-4 columns and the horizontal gutter.

We can get the width of one col-1-of-4 column using this:

calc((100% - 3 * $gutter-horizontal) / 4)

Now we simply multiply this by two and add the horizontal gutter to the result to get the width of the col-2-of-4 column like this:

.col-2-of-4 {
  width: calc(2 * ((100% - 3 * $gutter-horizontal) / 4) + $gutter-horizontal);
}

Calculating the width of the col-3-of-4 column

Row Width Col 3 of 4

From the image above, we see that the width of the col-3-of-4 column is the width of three col-1-of-4 columns and the width of two horizontal gutters.

We can get the width of one col-1-of-4 column using the code below:

calc((100% - 3 * $gutter-horizontal) / 4)

Now we simply multiply this by three and add two horizontal gutters (2 * $gutter-horizontal) to the result like this:

.col-2-of-4 {
  calc(3 * ((100% - 3 * $gutter-horizontal) / 4) + 2 * $gutter-horizontal)
}

The final SCSS code is given below:

// GRID
$grid-width: 100rem;
$gutter-vertical: 4rem;
$gutter-horizontal: 4rem;

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
   font-size: 10px
}

.row {
    max-width: $grid-width;
    margin: 0 auto;
    &:not(:last-child) {
        margin-bottom: $gutter-vertical;
    }

    &::after {
        content: "";
        display: table;
        clear: both;
    }

    [class^="col-"] {
        float: left;
        background-color: orange;
        padding: 1rem;
        &:not(:last-child) {
            margin-right: $gutter-horizontal;
        }
    }

    .col-1-of-2 {
        width: calc((100% - #{$gutter-horizontal}) / 2);

    }

    .col-1-of-3 {
        width: calc((100% - 2 * #{$gutter-horizontal}) / 3);
    }

    .col-2-of-3 {
        width: calc(2 * ((100% - 2 * #{$gutter-horizontal}) / 3) + #{$gutter-horizontal});
    }

    .col-1-of-4 {
        width: calc((100% - 3 * #{$gutter-horizontal}) / 4);
    }

    .col-2-of-4 {
        width: calc(2 * ((100% - 3 * #{$gutter-horizontal}) / 4) + #{$gutter-horizontal});
    }

    .col-3-of-4 {
        width: calc(3 * ((100% - 3 * #{$gutter-horizontal}) / 4) + 2 * #{$gutter-horizontal});
    }

}

It is important to note that in SCSS we have to wrap our variables in #{ } before they can work with the CSS calc function.

You can get all of the code for this grid on CodePen or by cloning this repository.

Conclusion

Our grid above covers a lot of scenarios and more can be added as the need arises since it is custom-built. While building this grid, we touched on some advanced CSS which I hope you find interesting. And although we built this grid system using float, it is also possible to use flexbox or CSS grid.

Lastly, I hope that by building the custom grid above, you know how grid systems are configured, how they work, and can now build your own custom grid.

Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web apps — .

Lawrence Eagles Senior full-stack developer, writer, and instructor.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

2 Replies to “How to build a custom grid with SASS”

  1. The title of this article is quite misleading. The techniques described are all based on old float/clear methods. Nothing to do with CSS Grid, or how it addresses css page layout. Waste of a click for me, and potentially confusing to a CSS beginner.

  2. Hello Adrian, Lawrence Eagles here.

    Truly the article does not directly talk about CSS Grid. It is aimed at teaching readers how CSS Grid works by building a custom grid — that has features similar to those in CSS Grid.

    The knowledge acquired during this process can then foster the reader’s understanding of CSS gird.

    I choose to use CSS float because it is straightforward.

    Below are some of our post on CSS Grid:

    https://blog.logrocket.com/full-bleed-layout-css-grid/

    https://blog.logrocket.com/how-to-use-css-grid-to-build-a-responsive-web-layout/

    Hope this helps.

Leave a Reply