In this article by Luke Watts, author of the book Mastering Sass, we will build a responsive grid system using the Susy library and a few custom mixins and functions. We will set a configuration map with our breakpoints which we will then loop over to automatically create our entire grid, using interpolation to create our class names.
(For more resources related to this topic, see here.)
For this example, we will need bower to download Susy. After Susy has been downloaded we will only need two files. We'll place them all in the same directory for simplicity. These files will be style.scss and _helpers.scss. We'll place the majority of our SCSS code in style.scss. First, we'll import susy and our _helpers.scss at the beginning of this file. After that we will place our variables and finally our code which will create our grid system.
To check if you have bower installed open your command line (Terminal on Unix or CMD on Windows) and run:
bower -v
If you see a number like "1.7.9" you have bower. If not you will need to install bower using npm, a package manager for NodeJS.
If you don't already have NodeJS installed, you can download it from: https://nodejs.org/en/.
To install bower from your command line using npm you will need to run:
npm install -g bower
Once bower is installed cd into the root of your project and run:
bower install susy
This will create a directory called bower_components. Inside that you will find a folder called susy. The full path to file we will be importing in style.scss is bower_components/susy/sass/_susy.scss. However we can leave off the underscore (_) and also the extension (.scss). Sass will still load import the file just fine. In style.scss add the following at the beginning of our file:
// style.scss
@import 'bower_components/susy/sass/susy';
Next, we'll need to import our _helpers.scss file in style.scss. Our _helpers.scss file will contain any custom mixins or functions we'll create to help us in building our grid.
In style.scss import _helpers.scss just below where we imported Susy:
// style.scss
@import 'bower_components/susy/sass/susy';
@import 'helpers';
I don't know about you, but writing media queries always seems like bit of a chore to me. I just don't like to write (min-width: 768px) all the time. So for that reason I'm going to include the bp mixin, which means instead of writing:
@media(min-width: 768px) {
// ...
}
We can simply use:
@include bp(md) {
// ...
}
First we are going to create a map of our breakpoints. Add the $breakpoints map to style.scss just below our imports:
// style.scss
@import 'bower_components/susy/sass/susy';
@import 'helpers';
$breakpoints: (
sm: 480px,
md: 768px,
lg: 980px
);
Then, inside _helpers.scss we're going to create our bp mixin which will handle creating our media queries from the $breakpoints map. Here's the breakpoint (bp) mixin:
@mixin bp($size: md) {
@media (min-width: map-get($breakpoints, $size)) {
@content;
}
}
Here we are setting the default breakpoint to be md (768px). We then use the built in Sass function map-get to get the relevant value using the key ($size). Inside our @media rule we use the @content directive which will allows us pass any Sass or CSS directly into our bp mixin to our @media rule.
The container mixin sets the max-width of the containing element, which will be the .container element for now. However, it is best to use the container mixin to semantically restrict certain parts of the design to your max width instead of using presentational classes like container or row.
The container mixin takes a width argument, which will be the max-width. It also automatically applies the micro-clearfix hack. This prevents the containers height from collapsing when the elements inside it are floated. I prefer the overflow: hidden method myself, but they do the same thing essentially.
By default, the container will be set to max-width: 100%. However, you can set it to be any valid unit of dimension, such as 60em, 1160px, 50%, 90vw, or whatever. As long as it's a valid CSS unit it will work.
In style.scss let's create our .container element using the container mixin:
// style.scss
.container {
@include container(1160px);
}
The preceding code will give the following CSS output:
.container {
max-width: 1160px;
margin-left: auto;
margin-right: auto;
}
.container:after {
content: " ";
display: block;
clear: both;
}
Due to the fact the container uses a max-width we don't need to specify different dimensions for various screen sizes. It will be 100% until the screen is above 1160px and then the max-width value will kick in. The .container:after rule is the micro-clearfix hack.
To create columns in Susy we use the span mixin. The span mixin sets the width of that element and applies a padding or margin depending on how Susy is set up. By default, Susy will apply a margin to the right of each column, but you can set it to be on the left, or to be padding on the left or right or padding or margin on both sides. Susy will do the necessary work to make everything work behind the scenes.
To create a half width column in a 12 column grid you would use:
.col-6 {
@include span(6 of 12);
}
The of 12 let's Susy know this is a 12 column grid. When we define our $susy map later we can tell Susy how many columns we are using via the columns property. This means we can drop the of 12 part and simply use span(6) instead. Susy will then know we are using 12 columns unless we explicitly pass another value.
The preceding SCSS will output:
.col-6 {
width: 49.15254%;
float: left;
margin-right: 1.69492%;
}
Notice the width and margin together would actually be 50.84746%, not 50% as you might expect. Therefor two of these column would actually be 101.69492%. That will cause the last column to wrap to the next row. To prevent this, you would need to remove the margin from the last column.
To address this, Susy uses the last keyword. When you pass this to the span mixin it lets Susy know this is the last column in a row. This removes the margin right and also floats the element in question to the right to ensure it's at the very end of the row.
Let's take the previous example where we would have two col-6 elements. We could create a class of col-6-last and apply the last keyword to that span mixin:
.col-6 {
@include span(6 of 12);
&-last {
@include span(last 6 of 12)
}
}
The preceding SCSS will output:
.col-6 {
width: 49.15254%;
float: left;
margin-right: 1.69492%;
}
.col-6-last {
width: 49.15254%;
float: right;
margin-right: 0;
}
You can also place the last keyword at the end. This will also work:
.col-6 {
@include span(6 of 12);
&-last {
@include span(6 of 12 last)
}
}
Susy allows for a lot of configuration through its configuration map which is defined as $susy. The settings in the $susy map allow us to set how wide the container should be, how many columns our grid should have, how wide the gutters are, whether those gutters should be margins or padding, and whether the gutters should be on the left, right or both sides of each column. Actually, there are even more settings available depending what type of grid you'd like to build. Let's, define our $susy map with the container set to 1160px just after our $breakpoints map:
// style.scss
$susy: (
container: 1160px,
columns: 12,
gutters: 1/3
);
Here we've set our containers max-width to be 1160px. This is used when we use the container mixin without entering a value. We've also set our grid to be 12 columns with the gutters, (padding or margin) to be 1/3 the width of a column.
That's about all we need to set for our purposes, however, Susy has a lot more to offer. In fact, to cover everything in Susy would need an entirely book of its own. If you want to explore more of what Susy can do you should read the documentation at http://susydocs.oddbird.net/en/latest/.
We've all used a 12 column grid which has various sizes (small, medium, large) or a set breakpoint (or breakpoints). These are the most popular methods for two reasons...it works, and it's easy to understand. Furthermore, with the help of Susy we can achieve this with less than 30 lines of Sass! Don't believe me? Let's begin.
Our grid system will be similar to that of Foundation and Bootstrap. It will have 3 breakpoints and will be mobile-first. It will have a container, which will act as both .container and .row, therefore removing the need for a .row class.
Earlier we defined three sizes in our $breakpoints map. These were:
$breakpoints: (
sm: 480px,
md: 768px,
lg: 980px
);
So our grid will have small, medium and large breakpoints.
Our columns will use a similar naming convention to that of Bootstrap. There will be four available sets of columns.
Having four options will give us the most flexibility.
From here we can use an @for loop and our bp mixin to create our four sets of classes. Each will go from 1 through 12 (or whatever our Susy columns property is set to) and will use the breakpoints we defined for small (sm), medium (md) and large (lg).
In style.scss add the following:
// style.scss
@for $i from 1 through map-get($susy, columns) {
.col-#{$i} {
@include span($i);
&-last {
@include span($i last);
}
}
}
These 9 lines of code are responsible for our mobile-first set of column classes. This loops from one through 12 (which is currently the value of the $susy columns property) and creates a class for each. It also adds a class which handles removing the final columns right margin so our last column doesn't wrap onto a new line. Having control of when this happens will give us the most control. The preceding code would create:
.col-1 {
width: 6.38298%;
float: left;
margin-right: 2.12766%;
}
.col-1-last {
width: 6.38298%;
float: right;
margin-right: 0;
}
/* 2, 3, 4, and so on up to col-12 */
That means our loop which is only 9 lines of Sass will generate 144 lines of CSS! Now let's create our 3 breakpoints. We'll use an @each loop to get the sizes from our $breakpoints map. This will mean if we add another breakpoint, such as extra-large (xl) it will automatically create the correct set of classes for that size.
@each $size, $value in $breakpoints {
// Breakpoint will go here and will use $size
}
Here we're looping over the $breakpoints map and setting a $size variable and a $value variable. The $value variable will not be used, however the $size variable will be set to small, medium and large for each respective loop. We can then use that to set our bp mixin accordingly:
@each $size, $value in $breakpoints {
@include bp($size) {
// The @for loop will go here similar to the above @for loop...
}
}
Now, each loop will set a breakpoint for small, medium and large, and any additional sizes we might add in the future will be generated automatically. Now we can use the same @for loop inside the bp mixin with one small change, we'll add a size to the class name:
@each $size, $value in $breakpoints {
@include bp($size) {
@for $i from 1 through map-get($susy, columns) {
.col-#{$i}-#{$size} {
@include span($i);
&-last {
@include span($i last);
}
}
}
}
}
That's everything we need for our grid system. Here's the full stye.scss file:
/ /style.scss
@import 'bower_components/susy/sass/susy';
@import 'helpers';
$breakpoints: (
sm: 480px,
md: 768px,
lg: 980px
);
$susy: (
container: 1160px,
columns: 12,
gutters: 1/3
);
.container {
@include container;
}
@for $i from 1 through map-get($susy, columns) {
.col-#{$i} {
@include span($i);
&-last {
@include span($i last);
}
}
}
@each $size, $value in $breakpoints {
@include bp($size) {
@for $i from 1 through map-get($susy, columns) {
.col-#{$i}-#{$size} {
@include span($i);
&-last {
@include span($i last);
}
}
}
}
}
With our bp mixin that's 45 lines of SCSS. And how many lines of CSS does that generate? Nearly 600 lines of CSS! Also, like I've said, if we wanted to create another breakpoint it would only require a change to the $breakpoint map. Then, if we wanted to have 16 columns instead we would only need to the $susy columns property. The above code would then automatically loop over each and create the correct amount of columns for each breakpoint.
Next we need to check our grid works. We mainly want to check a few column sizes for each breakpoint and we want to be sure our last keyword is doing what we expect. I've created a simple piece of HTML to do this. I've also add a small bit of CSS to the file to correct box-sizing issues which will happen because of the additional 1px border. I've also restricted the height so text which wraps to a second line won't affect the heights. This is simply so everything remains in line so it's easy to see our widths are working. I don't recommend setting heights on elements. EVER. Instead using padding or line-height if you can to give an element more height and let the content dictate the size of the element.
Create a file called index.html in the root of the project and inside add the following:
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="UTF-8">
<title>Susy Grid Test</title>
<link rel="stylesheet" type="text/css" href="style.css" />
<style type="text/css">
*, *::before, *::after {
box-sizing: border-box;
}
[class^="col"] {
height: 1.5em;
background-color: grey;
border: 1px solid black;
}
</style>
</head>
<body>
<div class="container">
<h1>Grid</h1>
<div class="col-12 col-10-sm col-2-md col-10-lg">.col-sm-10.col-2-md.col-10-lg</div>
<div class="col-12 col-2-sm-last col-10-md-last col-2-lg-last">.col-sm-2-last.col-10-md-last.col-2-lg-last</div>
<div class="col-12 col-9-sm col-3-md col-9-lg">.col-sm-9.col-3-md.col-9-lg</div>
<div class="col-12 col-3-sm-last col-9-md-last col-3-lg-last">.col-sm-3-last.col-9-md-last.col-3-lg-last</div>
<div class="col-12 col-8-sm col-4-md col-8-lg">.col-sm-8.col-4-md.col-8-lg</div>
<div class="col-12 col-4-sm-last col-8-md-last col-4-lg-last">.col-sm-4-last.col-8-md-last.col-4-lg-last</div>
<div class="col-12 col-7-sm col-md-5 col-7-lg">.col-sm-7.col-md-5.col-7-lg</div>
<div class="col-12 col-5-sm-last col-7-md-last col-5-lg-last">.col-sm-5-last.col-7-md-last.col-5-lg-last</div>
<div class="col-12 col-6-sm col-6-md col-6-lg">.col-sm-6.col-6-md.col-6-lg</div>
<div class="col-12 col-6-sm-last col-6-md-last col-6-lg-last">.col-sm-6-last.col-6-md-last.col-6-lg-last</div>
</div>
</body>
</html>
Use your dev tools responsive tools or simply resize the browser from full size down to around 320px and you'll see our grid works as expected.
In this article we used Susy grids as well as a simple breakpoint mixin (bp) to create a solid, flexible grid system. With just under 50 lines of Sass we generated our grid system which consists of almost 600 lines of CSS.
Further resources on this subject: