Columns in the Hall of the Men At Arms, Conciergerie, Paris

Responsive Grid Building with Sass and Zen Grids: The Tale of the Breakpoint Grid Breakdown mixin

A common scenario in the world of responsive design: a grid of elements needs to have different numbers of columns depending on the screen width. This can easily be accomplished using media queries and clever selectors, but what if the same effect is needed on another list of elements? With Sass, you might create a mixin to avoid rearranging your styles or even worse, copying and pasting those rules.

Let's take it to the next level. Other grids with different selectors need different numbers of columns across different breakpoints. What then? Create another, almost identical mixin? No! You're copying and pasting again! Instead, create one mixin that can be passed a few variables and sit back while it does all the work for you. How? Keep reading!

Before we get started, it's worth mentioning that this specific example is tailored to Sass and Zen Grids. If you are not using Sass, we'd highly recommend it. It just might change your life. As for Zen Grids, we've been using it a lot lately. If you're using a different responsive grid framework, that's cool, but you'll need to adapt small portions of the final mixin.

For those who are skimming: jump to the meat and potatoes of the article.

Creating a breakpoint mixin

If you aren't already doing something similar, this is a slick strategy for styling one element across multiple breakpoints.

First, create a simple mixin like so:


@mixin breakpoint($breakpoint-size) {
  @media all and (min-width: $breakpoint-size) {
    @content;
  }
}

You can pass that mixin pixel values, but if you're using it a lot (you will be), you'll probably want to define some variables. We don't like using device-centric names for ours (they aren't future-friendly), but we're going to here to keep things easy to understand.

Maybe you define these breakpoints:


$breakpoint-mobile: 320px;
$breakpoint-tablet: 768px;
$breakpoint-desktop: 960px;

As a general strategy, we prefer to use these mixins multiple times within a given element rather than spread the same element across a few "master" media queries. Notice we used min-width in the mixin above. By listing the breakpoints from small to large, the styles become additive and will override the same rules in smaller breakpoints.

Using the mixin could look something like this:


.headline {
  // Global and mobile styles
  font-family: sans-serif;
  font-size: 1em;

  @include breakpoint($breakpoint-mobile) {
    // Styles from 480px on up. Can override those previously defined.
    font-size: 1.25em;
    font-weight: bold;
  }
  @include breakpoint($breakpoint-tablet) {
    // Styles from 768px on up. Again, can override.
    font-size: 1.5em;
  }
  @include breakpoint($breakpoint-desktop) {
    // Styles from 960px on up. Overrides everything!
    font-size: 2em;
    font-style: italic;
  }
}

Keep in mind that you might use more (or less) breakpoints in any given element. You don't have to use every breakpoint in every element - just use what you need in each one.

This example produces the following CSS:


.headline {
  font-family: sans-serif;
  font-size: 1em;
}
@media all and (min-width: 320px) {
  .headline {
    font-size: 1.25em;
    font-weight: bold;
  }
}
@media all and (min-width: 768px) {
  .headline {
    font-size: 1.5em;
  }
}
@media all and (min-width: 960px) {
  .headline {
    font-size: 2em;
    font-style: italic;
  }
}

That gets us through using a simple mixin to change styles across breakpoints. Awesome!

Dynamic grid creation with Breakpoint Grid Breakdown

Now is when things get exciting. Remember the scenario that we proposed at the beginning of the article? We need to create one mixin that can accomodate styles for different grids with different numbers of columns across different breakpoints.

Note: the most common use case for this is styling a View. When doing so, don't use the Grid format! Use "HTML list" or "Unformatted" and this mixin will build the grid for you.

Here we go (with a heavy dose of comments):


// Breakpoint Grid Breakdown
// Displays a grid of items with a dynamic column count across breakpoints.
// For use with Zen Grids and a breakpoint-oriented mixin.
// @author Gus Childs http://drupal.org/user/1468898
//
// @param list $column-counts
//   A list of how many columns should exist on the respective breakpoints.
// @param list $breakpoints
//   A list of breakpoints to be used in the 'breakpoint' mixin. Corresponds
//   directly with the $column-counts list. Defaults to those commonly used.
// @param string $selector
//   The selector of each individual grid item. Defaults to 'views-row'.
@mixin breakpoint-grid-breakdown($column-counts, $breakpoints: $breakpoint-mobile $breakpoint-tablet $breakpoint-desktop, $selector: '.views-row') {
  // The selector, such as '.views-row'.
  #{$selector} {
    // Loop through the breakpoints specified.
    @each $breakpoint in $breakpoints {
      // Which breakpoint are we currently on?
      $key: index($breakpoints, $breakpoint);
      // How many columns should exist in that breakpoint?
      $column-count: nth($column-counts, $key);

      // Uses our breakpoint mixin to specify what should happen on the current
      // breakpoint in the loop.
      // Important: Adjust this if using a different mixin or strategy.
      @include breakpoint($breakpoint) {
        // Loop through the number of columns on this breakpoint.
        @for $i from 1 through $column-count {
          // Creates :nth-child selectors for each column. For example, in four
          // columns we would have the following selectors here:
          // &:nth-child(4n+1), &:nth-child(4n+2),
          // &:nth-child(4n+3), and &:nth-child(4n+0).
          $remainder: $i % $column-count;
          &:nth-child(#{$column-count}n+#{$remainder}) {
            // Important: this relies on the use of $zen-column-count, which is
            // specific to Zen Grids. $zen-column-count should be replaced with
            // a variable that represents how many columns you're using
            // within the current breakpoint (or globally).

            // How many columns of the general grid on my page should one
            // column of this specific grid take up? For example, if the whole
            // page uses 12 columns, and we have a 3 column grid going for
            // these elements, $page-grid-column-span would be 4.
            $page-grid-column-span: $zen-column-count / $column-count;
            // Which column should this specific column start on?
            $page-grid-column-position: 1 + (($i - 1) * $page-grid-column-span);

            // Important: adapt this if you aren't using Zen Grids!
            // If you are: this is where all the magic happens.
            // http://zengrids.com/help/#zen-grid-item
            @include zen-grid-item($page-grid-column-span, $page-grid-column-position);

            // Clear the first item in every row so they don't stack on top
            // of each other.
            @if $remainder == 1 or $column-count == 1 {
              clear: both;
            } @else {
              clear: none;
            }
          }
        }
      }
    }
  }

  // A clearfix so elements following this grid will be placed correctly.
  &:after {
    content: "";
    display: table;
    clear: both;
  }
}

With that breakpoint, your styles elsewhere become as simple as this:


// Create a grid of unformatted views rows that has one column on mobile, two
// on tablet, and four on desktop.
.view-id-fancy-grid {
  @include breakpoint-grid-breakdown(1 2 4);
}
// Create a grid of unformatted list items that has one column on mobile and
// tablet, and two on desktop.
.list-of-items {
  @include breakpoint-grid-breakdown(1 2, $breakpoint-mobile $breakpoint-desktop, '.list-item');
}

The first example will generate the following CSS structure using Compass:


@media all and (min-width: 320px) {
  .view-id-fancy-grid .views-row:nth-child(1n+0) {
    /* Zen Grid output to position the only column (removed for simplicity). */
    clear: both;
  }
}
@media all and (min-width: 768px) {
  .view-id-fancy-grid .views-row:nth-child(2n+1) {
    /* Zen Grid output to position column 1 (removed for simplicity). */
    clear: both;
  }
  .view-id-fancy-grid .views-row:nth-child(2n+0) {
    /* Zen Grid output to position column 2 (removed for simplicity). */
    clear: none;
  }
}
@media all and (min-width: 960px) {
  .view-id-fancy-grid .views-row:nth-child(4n+1) {
    /* Zen Grid output to position column 1 (removed for simplicity). */
    clear: both;
  }
  .view-id-fancy-grid .views-row:nth-child(4n+2) {
    /* Zen Grid output to position column 2 (removed for simplicity). */
    clear: none;
  }
  .view-id-fancy-grid .views-row:nth-child(4n+3) {
    /* Zen Grid output to position column 3 (removed for simplicity). */
    clear: none;
  }
  .view-id-fancy-grid .views-row:nth-child(4n+0) {
    /* Zen Grid output to position column 4 (removed for simplicity). */
    clear: none;
  }
}
.view-id-fancy-grid:after {
  content: "";
  display: table;
  clear: both;
}

The part that really makes the magic happen is the use of :nth-child. The Internet Explorer wizards among us may remember that IE 8 and lower don't support :nth-child by default, so you'll need to use Selectivizr to do so. There's a Selectivizr module if you need it.

Less comments, please!

For the brave souls who have wrapped their brains around what's going on:


// Breakpoint Grid Breakdown
// Displays a grid of items with a dynamic column count across breakpoints.
// For use with Zen Grids and a breakpoint-oriented mixin.
// @author Gus Childs http://drupal.org/user/1468898
//
// @param list $column-counts
//   A list of how many columns should exist on the respective breakpoints.
// @param list $breakpoints
//   A list of breakpoints to be used in the 'breakpoint' mixin. Corresponds
//   directly with the $column-counts list. Defaults to those commonly used.
// @param string $selector
//   The selector of each individual grid item. Defaults to 'views-row'.
@mixin breakpoint-grid-breakdown($column-counts, $breakpoints: $breakpoint-mobile $breakpoint-tablet $breakpoint-desktop, $selector: '.views-row') {
  #{$selector} {
    @each $breakpoint in $breakpoints {
      $key: index($breakpoints, $breakpoint);
      $column-count: nth($column-counts, $key);

      @include breakpoint($breakpoint) {
        @for $i from 1 through $column-count {
          $remainder: $i % $column-count;
          &:nth-child(#{$column-count}n+#{$remainder}) {
            $page-grid-column-span: $zen-column-count / $column-count;
            $page-grid-column-position: 1 + (($i - 1) * $page-grid-column-span);
            @include zen-grid-item($page-grid-column-span, $page-grid-column-position);
            @if $remainder == 1 or $column-count == 1 {
              clear: both;
            } @else {
              clear: none;
            }
          }
        }
      }
    }
  }

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

Wrapping up

We understand that this can be overwhelming for someone who isn't extremely familiar with Sass and Zen Grids. While this won't be a plug-and-play solution for the faint of heart, we hope it will get some folks thinking and set others out on the right foot. If you end up using this on a project or have any suggestions for improvements, we'd love to hear about it!

If you'd like to easily grab the code or perhaps even submit a pull request, it's on GitHub!

Front-End Development CSS Sass
Gus Childs Headshot

Gus is a mobile first front-end developer that prioritizes user experience. He isn't afraid to write a module and is a community contributor and speaker.