Before we even get started talking about Sass and the code for this project, let me remind you this is no more than a quick experiment which by no means should be used in a live context. So what follows is only a proof of concept of what we could do with a couple of Sass variables and the calc()
function from CSS.
There are probably still quite a couple of flaws and edge cases that are not handled in this example. If you have any recommendations for improving this, be sure to share. Otherwise keep in mind it’s just an experiment.
Key Takeaways
- This project is an experimental proof of concept for a grid system using Sass variables and the CSS calc() function, and it should not be used in a live context.
- The grid system uses three customizable variables: the number of columns in the grid, the width of a gutter between columns, and the screen width, under which the layout moves to a single column.
- The grid system avoids using a class name in the DOM and instead uses advanced CSS selectors like :nth-of-type to do everything from within the stylesheet.
- The grid system can handle nested grids and grids with an unknown number of children, making it flexible for different layout requirements.
- The grid system degrades gracefully on small screens, stacking everything as a single column below a given breakpoint.
What Are We Trying to Do?
In the last couple of days I have been working with the calc() function in CSS. calc()
can be a blessing when it comes to cross-unit calculations, and gets even better when you have a couple of Sass variables to make everything easily customizable.
Then someone on Twitter asked me how I would build a grid system if I had to. Firstly, I wouldn’t. There are too many CSS grid systems and frameworks already, and building another one would be reinventing the wheel. In fact, I once wrote an article called We don’t need your CSS grid, even if my opinion is now slightly more peaceful than when I first wrote the article. Long story short: Some smart people already built grid-systems that are more powerful than any grid system I could ever come up with (like Breakpoint and Susy).
Anyway, I didn’t want to do a grid system like the others so I thought “hey, why not have some fun with calc
”? I wanted to keep things as simple as they could be — three variables, one breakpoint, one mixin. That’s all. The three customizable variables are:
- The number of columns in the grid (default
$grid-columns: 12
) - The width of a gutter between columns (default
$grid-gutter: 10px
) - The screen width, under which we move to a single column (default
$grid-breakpoint: 48em
)
Another peculiarity I wanted to have (which is actually getting less and less special) is to avoid using a class name in the DOM. More importantly, I wanted to do everything from within the stylesheet. This also means I had to use advanced CSS selectors like :nth-of-type
.
Mixin Core
I always try to keep my function and mixin signatures as lean as possible. For this one, I ended up needing no more than a single argument:
@mixin grid($cols...) {
// Sass magic
}
… which is actually a variable number of arguments (AKA an argList
), as indicated by the ellipses after the $cols
variable. The main idea is to loop through those arguments and handle columns based on this thanks to the :nth-of-type
CSS selector. For instance, calling grid(6, 6)
on a 12-column based grid will create 2 columns separated by a 10px
gutter.
But before looping, let’s add a couple of declarations to build the layout:
@mixin grid($cols...) {
overflow: hidden;
> * {
float: left;
margin-right: $gutter;
}
// Loop
}
To target all children from the container calling the mixin, we use the *
selector with the child combinator >
. I bet some of you are gasping already. Well… yes. It’s 2014, which means CSS performance is not a problem anymore. Also, since we’re using calc()
, we won’t be supporting anything below Internet Explorer 9, so we’re using > *
, alright!
In our declaration block we float all immediate child elements and add a right margin. The wrapper has overflow: hidden
to clear inner floats. If you’re more of a micro-clearfix person, be sure to change this to suit your needs. In case it’s a list, don’t forget to add list-style: none
and padding-left: 0
, if your CSS reset is not already doing so.
Now, the loop!
@for $i from 1 through length($cols) {
> :nth-of-type(#{$i}n) {
$multiplier: $i / $grid-columns;
width: calc(100% * #{$multiplier} - #{$grid-gutter} * (1 - #{$multiplier}));
}
}
Ouch, this looks complicated as hell! Let’s deal with this one line at a time. For the whole explanation, let’s assume we are calling grid(3, 7, 2)
, which would be a pretty standard layout with a central container circled with 2 sidebars. And don’t forget we have a 12-column layout with 10px gutters, as we’ve defined in our variables earlier.
First, the selector. Don’t pay attention to the n
yet, we’ll see why it’s there in the next section. For now all you have to understand is we select children one by one to apply a width to them. By the way, the empty space before :nth-of-type
is an implicit *
.
Now the calc()
function. The calculation looks pretty intense, doesn’t it? Actually it’s not that hard to understand if you picture it. Let’s deal with our first column (3
). If we go through our equation step by step, here is what we get:
- 100% * 3 / 12 – 10px * (1 – 3 / 12)
- 100% * 0.25 – 10px * 0.75
- 25% – 7.5px
Our column will spread over 25% of the total width minus 7.5 pixels (hopefully targeted browsers will deal with subpixel rendering). Still not clear? Let’s see another example with our main container (7
):
- 100% * 7 / 12 – 10px * (1 – 7 / 12)
- 100% * 0.5833333333333334 – 10px * 0.41666666666666663
- 58.33333333333334% – 4.1666666666666663px
And last but not least, our right sidebar (2
):
- 100% * 2 / 12 – 10px * (1 – 2 / 12)
- 100% * 0.16666666666666666 – 10px * 0.8333333333333334
- 16.666666666666666% – 8.333333333333334px
Now if we add the three results to make sure everything’s right:
- (25% – 7.5%) + (58.33333333333334% – 4.1666666666666663px) + (16.666666666666666% – 8.333333333333334px)
- (25% + 58.33333333333334% + 16.666666666666666%) – (4.1666666666666663px + 8.333333333333334px + 7.5px)
- 100% – 20px
Which makes sense since we have 2 gutters of 10px. That’s all for the calculations folks. It wasn’t that difficult, was it?
Last important thing: We remove the right margin from the last child outside of the loop with another advanced CSS selector:
> :nth-of-type(#{length($cols)}n) {
margin-right: 0;
}
In case you’re wondering, applying margin to all children, then removing margin from last child, is better than just applying margin on every child except the last. I tried both.
Note: when using Sass variables in the calc()
function, don’t forget to interpolate them if you want it to work. Remember calc()
is not compiled by Sass but by the browser itself so it needs to have all values properly written in the function.
Unknown Number of Items
I suppose it is needless to say that the grid system handles nested grids quite well. One thing I wanted was the ability to have nested grids with an unknown number of children. There are numerous reasons for this, whether it be Ajax loading, lazyload, or whatever.
Because we don’t know the number of children, we can’t include the “ratio” for all children in the mixin inclusion. So I came up with a solution only requiring the pattern of a single row (e.g. grid(3, 3, 3, 3)
). Then if there are more than 4 children, they should still behave like it’s a 4-column grid (new row and so on).
Also you may have noticed we are not using any sub-wrappers for each row since we don’t make any change to the DOM. So we need to make sure the last child of the container and the last child of each row each have no margin.
Hence the :nth-of-type()
selectors we’ve seen previously. This means for instance that children 4
, 8
, 12
, and so on won’t have a right margin.
Dealing with Small Screens
Now that we have everything working like a charm, we should make sure the grid degrades gracefully on small screens. I’m keeping it simple: Below the given breakpoint everything stacks as a single column.
@mixin grid($cols...) {
// Mixin core
@media (max-width: $breakpoint) {
float: none;
width: 100%;
margin-right: 0;
}
}
Below this screen width, elements behave as they would without the grid system. That is, block-level elements stretch to fit the width of their parent and get positioned one under in source order. A simple, yet efficient, approach.
Improving CSS Output with Placeholders
So far, it does the job very well. Everything works great and we are pretty happy, aren’t we? However if we happen to have multiple grids on the same page, there are a lot of duplicate CSS rules that could be merged to make output lighter.
We could make our mixin extend placeholders instead of directly dumping CSS rules. First, let’s create our placeholders.
%grid-parent {
overflow: hidden;
}
%grid-child {
float: left;
margin-right: $grid-gutter;
}
%grid-last-child {
margin-right: 0;
}
@for $i from 1 through $grid-columns {
%grid-col-#{$i} {
$multiplier: $i / $grid-columns;
width: calc(100% * #{$multiplier} - #{$grid-gutter} * (1 - #{$multiplier}));
}
}
@media (max-width: $grid-breakpoint) {
%grid-fallback {
float: none;
width: 100%;
margin-right: 0;
}
}
The first three placeholders speak for themselves. For the 4th placeholder, to avoid computing the width directly inside the mixin, we create as many placeholders as we need for our grid (e.g. 12 for 12-columns) with a @for
loop.
Regarding the %grid-fallback
placeholder, we have to instantiate it inside a media query in order to be able to extend it from within an equivalent media query elsewhere in the stylesheet. Indeed, Sass has some restrictions regarding cross-media @extend (i.e. it doesn’t work).
And here is what the mixin looks like now — doing no mare than extending placeholders:
@mixin grid($cols...) {
@extend %grid-parent;
> * {
@extend %grid-child;
@for $i from 1 through length($cols) {
&:nth-of-type(#{$i}n) {
@extend %grid-col-#{nth($cols, $i)};
}
}
&:nth-of-type(#{length($cols)}n) {
@extend %grid-last-child;
}
}
@media (max-width: $grid-breakpoint) {
@extend %grid-fallback;
}
}
Final thoughts
Hey, it was pretty intense in the end, wasn’t it? To be honest, at first I thought it would be easy, but it occurred to me I had to do some advanced Sass to keep the CSS output as clean as if it was written by hand. A good rule of thumb is that the output from your Sass files should be sensibly similar to the one you would have written by yourself.
See the Pen euKgi by SitePoint (@SitePoint) on CodePen.
Frequently Asked Questions about Creative Grid System with Sass and Calc
How does the Sass variable work in the CSS calc function?
The Sass variable in the CSS calc function works by allowing you to use dynamic values in your CSS. When you define a variable in Sass, you can use it in your CSS calc function. The variable will be replaced by its value when the CSS is compiled. This allows you to create more flexible and reusable styles. For example, you can define a variable for a common margin or padding value and use it in multiple places in your CSS. If you ever need to change that value, you only need to change it in one place.
What is the difference between Sass calculations and CSS calc() function?
Sass calculations and CSS calc() function both allow you to perform calculations to determine CSS values. However, they work in slightly different ways. Sass calculations are performed at compile time, meaning the calculations are done when the Sass is converted to CSS. On the other hand, CSS calc() function is performed at runtime, meaning the calculations are done in the browser when the page is rendered. This means that CSS calc() can use values that are not known until runtime, such as the width of the viewport.
How can I use the CSS calc() function in a Sass mixin?
You can use the CSS calc() function in a Sass mixin just like you would in any other CSS rule. The mixin can take parameters, which can be used in the calc() function. Here’s an example:@mixin calc-width($margin) {
width: calc(100% - #{$margin});
}
In this example, the mixin takes a margin parameter and uses it in the calc() function to calculate the width. You can then include this mixin in your CSS rules and pass in the desired margin.
Can I use Sass variables and functions inside the CSS calc() function?
Yes, you can use Sass variables and functions inside the CSS calc() function. However, because the calc() function is a CSS function, not a Sass function, you need to interpolate the variables and functions using the #{…} syntax. This tells Sass to replace the variable or function with its value before the CSS is compiled. Here’s an example:$padding: 10px;
div {
width: calc(100% - #{$padding});
}
In this example, the $padding variable is interpolated inside the calc() function.
How can I create a responsive grid system with Sass and calc()?
Creating a responsive grid system with Sass and calc() involves defining a set of columns and gutters, and then using the calc() function to calculate the width of each column. You can use Sass variables and mixins to make the grid system more flexible and reusable. Here’s a basic example:$columns: 12;
$gutter: 20px;
@mixin column($span) {
width: calc((100% / #{$columns} * #{$span}) - #{$gutter});
}
div {
@include column(6);
}
In this example, the column mixin calculates the width of a column based on the number of columns and the gutter width. The div will span 6 columns of the grid.
What are the benefits of using Sass with the CSS calc() function?
Using Sass with the CSS calc() function provides several benefits. First, it allows you to use variables and functions in your calculations, making your CSS more flexible and maintainable. Second, it allows you to perform calculations at compile time, which can improve performance by reducing the amount of work the browser has to do at runtime. Finally, it allows you to create more complex calculations than would be possible with CSS alone, such as calculations involving multiple units or complex mathematical expressions.
Can I use the CSS calc() function with other CSS properties?
Yes, the CSS calc() function can be used with any CSS property that accepts numerical values. This includes properties like width, height, margin, padding, font-size, line-height, and many others. The calc() function allows you to perform calculations involving different units, such as percentages and pixels, which can be very useful for creating responsive designs.
What are the limitations of the CSS calc() function?
While the CSS calc() function is very powerful, it does have some limitations. First, it is not supported in Internet Explorer 8 and earlier. Second, it can only perform basic arithmetic operations (addition, subtraction, multiplication, and division), and it does not support more complex mathematical functions. Finally, while it can perform calculations involving different units, the result is always in the same unit as the leftmost value.
How can I debug issues with the CSS calc() function in Sass?
Debugging issues with the CSS calc() function in Sass can be tricky because the calculations are performed at runtime, not at compile time. However, there are a few techniques you can use. First, you can use the Sass @debug directive to print the value of variables or expressions to the console. Second, you can use the Sass inspect() function to convert a value to a string, which can be useful for debugging complex expressions. Finally, you can use the CSS calc() function in a browser that supports it and use the browser’s developer tools to inspect the computed values.
Can I use the CSS calc() function in Sass loops?
Yes, you can use the CSS calc() function in Sass loops. This can be useful for generating a series of styles with calculated values. For example, you could use a loop to generate a series of widths for a grid system. Here’s an example:$columns: 12;
$gutter: 20px;
@for $i from 1 through $columns {
.col-#{$i} {
width: calc((100% / #{$columns} * #{$i}) - #{$gutter});
}
}
In this example, the loop generates a series of .col- classes with calculated widths.
Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/her.