DEV Community

Cover image for Simple ‘Mixin’ Alternative To Standard CSS Grids
Uma Chisom Augustin
Uma Chisom Augustin

Posted on

Simple ‘Mixin’ Alternative To Standard CSS Grids

Using CSS grid to generate layouts in a design provides developers with value, and makes things a lot easier for developers, but there are still some problems or downsides of using the CSS grid systems. The main reason designers and web developers use CSS grid systems is to make the generation of layouts easier and faster. But the question is “can the generation of layouts be made easier and faster, and still overcome these problems?”. Fortunately for us, the answer is “Yes!’, all thanks to CSS preprocessors such as LESS and SASS/SCSS that can work with CSS variables, and perform calculations and output CSS via ‘mixins’.

Outline

  • How CSS Grid System Works
  • Some Downsides Of Using CSS Grids
    • High amounts of unused code
    • Restrictions on layout
    • set number of overall columns
    • Non semantic markup
    • Restrictions on nesting
  • An Alternative Solution
  • Tackling The Classic Layout
    • Easy px to em/rem conversion
    • .Row() mixin
    • .Cols() mixin
  • Adding Padding And Margins
    • Adding padding
    • Adding wrapping padding
    • Adding column gutters
  • Nesting And Increasing Layout Complexity
  • Conclusion

How CSS Grid Systems Work

A grid system is usually made up of a specific number of columns, normally 12 or 16 columns, and the ability to set an element, like the div or class element, to a width of X columns. Using CSS grid systems to create layouts on a website can be really easy, and of course, there are many values you can get from using CSS grid systems, but there are also downsides to using the CSS grid system. You should not think that using the CSS grid is bad because it isn’t. But as a tool you will be using consistently in your codes, you should be able to know the pros and cons, to determine if it is right for a specific project.

Some Downsides Of Using CSS Grids

Some of the downsides of using CSS Grids include:

  • High amounts of unused code

    Grid classes could be anywhere from 200 to 700 lines of code, but even in a simple design, most of the codes may never be used.

  • Restrictions on layout

    Grids are already calculated, they tend to have set widths that are difficult to change, e.g. column widths, gutter widths. The design becomes restricted to working only those widths.

  • Set number of overall columns

    Already calculated grid classes normally uses 12 or 16 columns. Let’s say you wanted a different number of columns, well, that won’t be possible.

  • Non Semantic markup

    The use of grid classes tends to bring about the placement of numerous non-semantic class names all through a document, such as a “row” or “column”.

  • Restrictions on nesting

    Most times, grid systems can only have columns that are nested inside inside one another. This restricts the complexity and flexibility of layout generation.

An Alternative Solution

‘Mixins’ are available in both LESS and SASS/SCSS. Mixins are reusable packages of rules that outputs different CSS depending on the information that is passed through them for processing. If you are not yet familiar with the understanding of what preprocessors are, and how they work with mixins and variables, you can read about it from http://lesscss.org/. Before we get started, you should know that there are already some existing libraries of LESS and SASS mixins that are already solving the problems if CSS grids, but we are going to be taking a different approach.

Note:
I am going to be writing these mixins in LESS, because it is harder to make LESS perform sophisticated operations than SASS, so it is easier for SASS users to adapt LESS mixins.

Tackling The Classic Layout

Let’s create a layout via LESS mixins, keeping the amount of CSS required to a minimum, with no pixel values, and maintaining a semantic markup. Let’s look at the HTML below:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Classic Layout</title>
    <script type='text/javascript' src='js/modernizr.js'></script>
    <link rel="stylesheet" href="css/normalize.css">
    <link rel="stylesheet" href="css/classiclayout.css">
</head>
<body>
<header>

</header>
<main>
    <article>

    </article>
    <aside>

    </aside>
</main>
<footer>

</footer>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

If you notice, there are no classes used here, only semantic HTML5 tags. I am going to be adding the following LESS code, using mixins and variables:

header, main, footer {
    .Row;
    background-color: #ccc;
    min-height: 200 * @toRems;
}

article {
    .Cols( 3 );
    min-height: 500 * @toRems;
    background-color: #ddd;
}

aside {
    .Cols( 1 );
    min-height: 500 * @toRems;
    background-color: #eee;
}
Enter fullscreen mode Exit fullscreen mode

it will then generate the following lines of CSS:

header,
main,
footer {
  max-width: 75rem;
  width: 100%;
  margin: 0 auto;
  background-color: #ccc;
  min-height: 12.5rem;
}
header:before,
main:before,
footer:before,
header:after,
main:after,
footer:after {
  content: "";
  display: table;
}
header:after,
main:after,
footer:after {
  clear: both;
}
article {
  width: 75%;
  float: left;
  min-height: 31.25rem;
  background-color: #ddd;
}
aside {
  width: 25%;
  float: left;
  min-height: 31.25rem;
  background-color: #eee;
}
Enter fullscreen mode Exit fullscreen mode

Check out the result on your browser, you will see a nice layout. Let’s check out the LESS mixins and variables that were used to create the layout you will be seeing in your browser:

//
// Variables for em / rem use
//

@base_px: 16; //set to the most common base px size used in browsers, should generally be left at default

@toRems: (1 / @base_px) + 0rem; //allows you to set values as the default px size to target, which is then converted into scalable rem values.

@toEms: (1 / @base_px) + 0em; //same as above, but with em values

//
// Grid mixins
//

@default-width: 1200 * @toRems;

@default-colspan: 1;

@default-total_cols: 4;

.Row ( @width : @default-width ) {
    max-width: @width;
    width: 100%;
    margin: 0 auto;
    // clear at the end of container
    &:before,
    &:after {
        content:"";
        display:table;
    }
    &:after {
        clear:both;
    }
}

.Cols ( @colspan : @default-colspan; @total_cols : @default-total_cols ) {
    width: ( @colspan * (100 / @total_cols) ) + 0%;
    float: left;
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s check out each elements one by one, as you can see above:

Easy px to em/rem conversion

//
// Variables for em / rem use
//

@base_px: 16; //set to the most common base px size used in browsers, should generally be left at default

@toRems: (1 / @base_px) + 0rem; //allows you to set values as the default px size to target, which is then converted into scalable rem values.

@toEms: (1 / @base_px) + 0em; //same as above, but with em values
Enter fullscreen mode Exit fullscreen mode

The variables that are in the top section of the code set us up for easily scalable em or rem values all through the stylesheet, instead of a px value. However, when we use these values, it allows us to conceptualize our design in pixels. For example, we do not want the header, main or footer elements to be wider than 1200px, so instead of specifying 1200px as the max-width, we have to covert that 1200px to rems by multiplying it with the @toREms variable, for example:

1200 * @toRems;
Enter fullscreen mode Exit fullscreen mode

The output value will be 75rem, which means that if a browser or user sets the default font-size to something different from the default size of 16px, then the entire site will scale proportionally. You can also do the same to generate em values, instead of using the @toEms variables.

.Row() mixin

@default-width: 1200 * @toRems;

.Row ( @width : @default-width ) {
    max-width: @width;
    width: 100%;
    margin: 0 auto;
    // clear at the end of container
    &:before,
    &:after {
        content:"";
        display:table;
    }
    &:after {
        clear:both;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the first mixin being used is the .Row() mixin. Instead of using the “.container” classes, you can call this mixin wherever you want an element to be centered with a maximum width. For our classic layout, we call this mixin on the header, main, and footer elements.

The mixin sets a max-width with a width of 100%. This will give us basic responsiveness by making the element automatically adjust to fill the available space when the viewport gets smaller than the max-width value. It also sets the margin to 0 auto making the elements to be automatically centered.

Lastly, it adds the pseudo-elements :before and :after and also uses them to automatically clear at the end of an element. This is required so that when adding columns inside the element, their float settings will be cleared.

The mixin accepts one parameter, a @width value:

.Row (@width : @default-width ) {
Enter fullscreen mode Exit fullscreen mode

The value now gets passed to the max-width property:

max-width: @width;
Enter fullscreen mode Exit fullscreen mode

If the mixin has a width parameter passed, e.g. .Row(40rem), the value will then be applied to the max-width property.

However, if the mixin is called without passing a parameter, i.e. .Row, then, the default value will be used. That default value is stored in the @default-width variable, which is set where you see:

@default-width: 1200 * @toRems;
Enter fullscreen mode Exit fullscreen mode

All this means that the default maximum width of any element this mixin is used on will be 1200px, converted into 1200rem. Using this mixin allows you to set any element to be centered at your default max-width, or any other max-width you may want to apply. What this means is that you can change the width of the entire site by changing the value of the @default-width variable. Let’s say for example:

@default-width: 800 * @toRems;
Enter fullscreen mode Exit fullscreen mode

.Cols() mixin

@default-colspan: 1;

@default-total_cols: 4;

.Cols ( @colspan : @default-colspan; @total_cols : @default-total_cols ) {
    width: ( @colspan * (100 / @total_cols) ) + 0%;
    float: left;
}
Enter fullscreen mode Exit fullscreen mode

Because we are not using fixed pixels, all our column widths will have to be percentage-based, because we want our layout to remain completely flexible. To calculate these percentages, we use .cols() mixin. According to the values you put into the mixin, a simple formula is used to determine the percentage width value that should be applied to the element. The element’s float value is set to left, so that columns will sit side by side (remember that we automatically clear floats that are applied to the columns via the .Row() mixin).

The .cols() mixin allows you to specify how many columns wide you want your element to be, the same thing you could if you were using the standard CSS grid system. This is normally done by passing a @colspan parameter through the mixin, or you can set the default for the mixin via the @default-colspan variable.

But, unlike most CSS grid systems, you have complete control over how many total that value is relative to, rather than having access to only 12 or 16 columns. You can set the total columns by passing a @total_cols parameter through the mixin, or you can just the mixin default via the @default-total_cols variable. Sounds cool right?

In the earlier example of a “classic” layout which I gave above, using CSS grid, the content area was set to 9 out of 12 available columns,(3/4), while the sidebar was set to 3 out of 12 columns(1/4). Well, we do not need all 12 columns, and we do not need to break the layout into twelve, all we need is quarters, because we are trying to set the content area to 3/4 width, and the sidebar to 1/4 of the width. So, all we have to do now is set the value of the @default-total_cols to a variable of 4. For example:

@default-total_cols: 4;
Enter fullscreen mode Exit fullscreen mode

Then, we are going to use the mixins as we did in our “classic” layout above. The mixins assume that you want your columns to have a possibility of 4 total columns. So, for us to set our article element to 3/4 width, you can do this:

article {
    .Cols( 3 );
}
Enter fullscreen mode Exit fullscreen mode

then, if you want to set the sidebar to 1/4 width, you can do this:

aside {
    .Cols( 1 );
}
Enter fullscreen mode Exit fullscreen mode

Let’s say you want to use a different number of total columns, we can do this by passing different values through the .cols() mixin. This will allow us to change the widths of the article and aside elements to any we want. Example:

article {
    .Cols( 7, 11 ); // sets this element to span 7 of a total 11 columns
    min-height: 500 * @toRems;
    background-color: #ddd;
}

aside {
    .Cols( 4, 11 ); // sets this element to span 4 of a total 11 columns
    min-height: 500 * @toRems;
    background-color: #eee;
}
Enter fullscreen mode Exit fullscreen mode

Awesome right?

Adding Padding And Margins

When you begin adding content inside your container, you definitely would want to have the ability to control the spacing around it. There are ways you can control your spacing, and the best to use depends on what you are trying to achieve with the specific design you are trying to create.

Adding Padding

One of the easiest ways you can control your spacing is by using the simple addition of padding parameters to both .Row() and .Cols() mixins. We are going to adjust our mixin code as shown below:

@default-padding: 0;

.Row ( @width : @default-width; @padding: @default-padding; ) {
    max-width: @width;
    width: 100%;
    margin: 0 auto;
    padding: @padding;
    // clear at the end of container
    &:before,
    &:after {
        content:"";
        display:table;
    }
    &:after {
        clear:both;
    }
}

.Cols ( @colspan : @default-colspan; @total_cols : @default-total_cols; @padding: @default-padding; ) {
    width: ( @colspan * (100 / @total_cols) ) + 0%;
    float: left;
    padding: @padding;
}
Enter fullscreen mode Exit fullscreen mode

We can now add padding to our header, article, aside, and footer elements. Note, we are also going to be setting the default box-sizing property all through the design to border-box so, padding does not get included in any browser calculation of the width of elements:

* {
    box-sizing:border-box;
    -moz-box-sizing:border-box;
}

header, footer {
    .Row ( 
        @padding: 20 * @toRems;
    );
    background-color: #ccc;
    min-height: 200 * @toRems;
}

main {
    .Row;
    background-color: #ccc;
}

article {
    .Cols (
        @colspan: 3;
        @padding: 10 * @toRems 20 * @toRems;
    );
    min-height: 500 * @toRems;
    background-color: #ddd;
}

aside {
    .Cols (
        @colspan: 1;
        @padding: 10 * @toRems 20 * @toRems;
    );
    min-height: 500 * @toRems;
    background-color: #eee;
}
Enter fullscreen mode Exit fullscreen mode

This will give us padding around each of our pieces of content, as shown below:

Adding Wrapper Margins

In some cases, the spacing that is intended to be added to a design is on the outside of an element, instead of the inside, for example, when you want a site’s background to show through between the elements. For this to happen, we have to make further additions to bot the .Row() and .Cols() mixins.

To get started, let’s allow spacing to be added above and below the elements that have our .Row() mixin applied to them. This can be done by simply replacing the existing margin property value with a variable that can be sent as a parameter all through the mixin.

@default-row_margin: 0 auto;

.Row ( @width : @default-width; @padding: @default-padding; @margin: @default-row_margin; ) {
    max-width: @width;
    width: 100%;
    margin: @margin;
    padding: @padding;
    // clear at the end of container
    &:before,
    &:after {
        content:"";
        display:table;
    }
    &:after {
        clear:both;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the default value for the row margin is still set to 0 auto, therefore, if no parameter is passed, the mixin will still center the element automatically.

If we pass an @margin value through the mixin:

header, footer {
    .Row ( 
        @padding: 20 * @toRems;
        @margin: 10 * @toRems auto;
    );
    background-color: #ccc;
    min-height: 200 * @toRems;
}
Enter fullscreen mode Exit fullscreen mode

…we are going to be able to add vertical spacing as see below:

Also, if you do not want your element to center anymore, all you have to do is pass a @margin value of 0 auto 0 0 to left align an element, or you can do 0 0 0 auto to right align.

Adding Column Gutters

One of the common features of CSS grid systems is its ability to add gutters between the columns, i.e. margins that apply between each columns but not to the outside of the outermost columns. Once again we can add this functionality with some additions to the Cols() mixin:

@default-gutter: 0;

.Cols ( @colspan : @default-colspan; @total_cols : @default-total_cols; @padding: @default-padding; @gutter: @default-gutter; @edge: false; ){
    @total_gutter: (@total_cols - 1) * @gutter;
    @spanned_gutters: (@colspan - 1) * @gutter;
    width: ( @colspan * ( (100 - @total_gutter) / @total_cols) ) + @spanned_gutters + 0%;
    float: left;
    padding: @padding;
    .IfEdge (@edge; @gutter);
}

.IfEdge ( @edge; @gutter; ) when (@edge = false) {
    margin-right: @gutter + 0%;
}

.IfEdge ( @edge; @gutter; ) when (@edge = true) {
    margin-right: 0;
}
Enter fullscreen mode Exit fullscreen mode

Now, the mixin does two extra things;

First, it checks for a value via the new @gutter parameter and goes on to factor it into the width calculation for the column.

Note: the @gutter ****value should be a number used as a percentage value. For example 2 for a 2% gutter.

Secondly, it goes on to check the new @edge variable to see is set to true or false. If it is set to false, the value of the @guttter parameter is added to the right margin as a percentage. IF it is set to true, the right margin is set to 0. This will allow you to specify where a column is at the edge of your layout and therefore, should not have an applied gutter.

In the example I will be showing you below, I will be showing you the effect of this change on the mixin. I have added two extra article element to our HTML. Also, I have adjusted the LESS for the article and aside elements, as shown below;

article {
    .Cols (
        @colspan: 1;
        @padding: 10 * @toRems 20 * @toRems;
        @gutter: 1; //include a gutter of 1%
    );
    min-height: 500 * @toRems;
    background-color: #ddd;
}

aside {
    .Cols (
        @colspan: 1;
        @padding: 10 * @toRems 20 * @toRems;
        @gutter: 1; //include a gutter of 1%
        @edge: true; //this is the column on the edge so don't set a right margin
    );
    min-height: 500 * @toRems;
    background-color: #eee;
}
Enter fullscreen mode Exit fullscreen mode

the result is this;

To allow for control over vertical margins on columns, all we have to do is include some additional parameters in the mixin as shown below:

@default-margin_top: 0;

@default-margin_bottom: 0;

.Cols ( @colspan : @default-colspan; @total_cols : @default-total_cols; @padding: @default-padding; @gutter: @default-gutter; @edge: false; @margin_top : @default-margin_top; @margin_bottom : @default-margin_bottom; ){
    @total_gutter: (@total_cols - 1) * @gutter;
    @spanned_gutters: (@colspan - 1) * @gutter;
    width: ( @colspan * ( (100 - @total_gutter) / @total_cols) ) + @spanned_gutters + 0%;
    float: left;
    padding: @padding;
    .IfEdge (@edge; @gutter; @margin_top; @margin_bottom; );
}

.IfEdge ( @edge; @gutter; @margin_top; @margin_bottom;  ) when (@edge = false) {
    margin: @margin_top @gutter + 0% @margin_bottom 0;
}

.IfEdge ( @edge; @gutter; @margin_top; @margin_bottom;  ) when (@edge = true) {
    margin: @margin_top 0 @margin_bottom 0;
} 
Enter fullscreen mode Exit fullscreen mode

The top and bottom margin settings can now be included when we use the .Cols() mixin, as shown below:

article {
    .Cols (
        @colspan: 1;
        @padding: 10 * @toRems 20 * @toRems;
        @gutter: 1;
        @margin_top: 20 * @toRems;
        @margin_bottom: 30 * @toRems;
    );
    min-height: 500 * @toRems;
    background-color: #ddd;
}

aside {
    .Cols (
        @colspan: 1;
        @padding: 10 * @toRems 20 * @toRems;
        @gutter: 1;
        @edge: true;
        @margin_top: 20 * @toRems;
        @margin_bottom: 30 * @toRems;
    );
    min-height: 500 * @toRems;
    background-color: #eee;
}
Enter fullscreen mode Exit fullscreen mode

…this would add vertical margins to the columns, as shown in the example below:

Nesting And Increasing Layout Complexity

To add another level of complexity to our example layout, we have to increase the number of article elements to 6 and place them inside a section element wrapper, then we can apply our mixins to create the layout in the example below:

I have made changes to the HTML to get the layout above, as you can see below:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Classic Layout</title>
    <script type='text/javascript' src='js/modernizr.js'></script>
    <link rel="stylesheet" href="css/normalize.css">
    <link rel="stylesheet" href="css/classiclayout.css">
</head>
<body>
<header>
    <h1>Site Title</h1>
</header>
<main>
    <section>
        <h1>Latest Articles</h1>
        <article>
            <h1>Article Title</h1>
            <p>...</p>
        </article>
        <article>
            <h1>Article Title</h1>
            <p>...</p>
        </article>
        <article>
            <h1>Article Title</h1>
            <p>...</p>
        </article>
        <article>
            <h1>Article Title</h1>
            <p>...</p>
        </article>
        <article>
            <h1>Article Title</h1>
            <p>...</p>
        </article>
        <article>
            <h1>Article Title</h1>
            <p>...</p>
        </article>
    </section>
    <aside>
        <p>...</p>
    </aside>
</main>
<footer>
    <p>Example Footer</p>
</footer>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

We are also going to change the LESS code we are using to control the layouts as shown below:

header, footer {
    .Row ( @padding: 20 * @toRems; @margin: 10 * @toRems auto; );
    background-color: #ccc;
    min-height: 100 * @toRems;
}

main {
    .Row;
}

section {
    .Cols ( @colspan: 3; @padding: 10 * @toRems 20 * @toRems; @gutter: 1; );
    background-color: #ddd;
}

aside {
    .Cols ( @colspan: 1; @padding: 10 * @toRems 20 * @toRems; @gutter: 1; @edge: true; );
    min-height: 500 * @toRems;
    background-color: #eee;
}

article {
    .Cols ( @colspan: 1; @total_cols: 3; @padding: 0 20 * @toRems 20 * @toRems 20 * @toRems; @margin_bottom: 20 * @toRems;  @gutter: 2; );
    background-color: #eee;
    &:nth-of-type(3n) {
        margin-right: 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see above, the section element has been set to take up 3 columns of the default of 4 total columns(3/4). Next to the section is the aside element, which is set to 1 column of 4(1/4). The section element acts as a wrapper for the article. Since the article elements are now nested, they can have all the new column width applied to them, and they will now each take up a percentage of their parent element’s interior. Therefore, each is set to 1 column out of 3, with a gutter of 2%.

All third article is identified by using the :nth-of-type(3n) selector and set to have no right margin/gutter.

Conclusion

We have gone through so many details in this article on how the .Row and .Cols mixins work so that you can have an understanding, for you to be able to make further additions and modifications if you want. The result of all we have covered is actually a simple and easy-to-use layout system.

By using just two mixins, you can now create highly complex, but scalable and flexible layouts, without restrictions. You can also use any total number of columns you feel like, width of gutters, and you can nest your grids as much as needed, etc.


Top comments (3)

Collapse
 
hakimio profile image
Tomas Rimkus

Most of the CSS grid downsides you mention are simply not true. Instead of bashing it, you should use some time to actually learn how to use it. CSS grid allows you to easily define fully responsive layouts with very little code. Maybe you are mistaken Bootstrap grid layout with CSS grid? Also, using "display: table" and "float" for main page layout might have been a good idea sometime in the last century, but definitely is not nowadays.

Recommended resources to learn CSS grid:
Common Responsive Layouts with CSS Grid
Learn CSS Grid
A Complete Guide to Grid

Collapse
 
naveennamani profile image
naveennamani

I'm about to comment the same, it seems the author just showing off his LESS skills to reinvent the grid system instead of understanding and using it. I'm sure even we don't use the css grid system use cases like templates, subgrid etc. most of the times, yet we can achieve what we want in less than 20-30 lines and not 200-300 lines.

Collapse
 
shikkaba profile image
Me

Sorry, but floats for layout at this point in time? That just over complicates everything. If you don't want to use grid, use flexbox.

Also, your points on grid are not accurate. In a custom site, all your code would be used. You can adapt the sizes of each grid column, and even then, you can fit grid content into multiple columns or rows. It is very flexible.
Changing the number of columns is possible. Even then, you can make it so items can span multiple columns so you can have different visual column designs without adding a new container (ie. A 12 column can be 1, 2, 3, 4, 6, 7, 12 columns just from where you assign item).

I suggest you look into it some more.
css-tricks.com/snippets/css/comple...
developer.mozilla.org/en-US/docs/L...
developer.mozilla.org/en-US/docs/W...
learncssgrid.com/