On Keeping Breakpoints DRY

Avatar of Eduardo Bouças
Eduardo Bouças on (Updated on )

The following is a guest post by Eduardo Bouças. Eduardo is back to follow up on his journey of approaching media queries programatically. He’ll catch you up on how this started, where it’s went, and how that’s going.

A year ago, I went on a personal quest to find the best way to use the powers of CSS preprocessors to manage breakpoints and write media queries in my projects. The outcome was include-media, a library that promises “simple, elegant and maintainable” media queries in Sass. But how did that work out for me?

The first two adjectives above are actually quite subjective, since simplicity and elegance eventually boil down to personal preference — there’s no point in talking about those. Instead, I’d like to share with you my personal experience of using the library for a year on commercial projects, and how it particularly helped me with the third aspect: maintainability.

Back to the basics

The need for something like include-media comes from a problem that developers absolutely hate: repeating code. Until element queries become a thing and we start thinking of responsive design at a component level, we’re stuck with media queries that respond to changes in the global viewport, not in the context of the element.

As a result, developers often define “global breakpoints”, which are really just arbitrary viewport sizes at which elements in a page may change form. Even though that’s a simple concept that can be easily implemented in vanilla CSS, there’s an immediate advantage that a pre-processor can bring to the table: the ability to define these breakpoints dynamically as variables, allowing developers to declare them just once and reference them everywhere.

$breakpoints: (
  'small': 400px,
  'medium': 900px,
  'large': 1200px
);

Over the last year, I’ve been focusing on taking that principle even further, by making these breakpoint declarations available in different contexts.

Enter JavaScript

It’s not uncommon to reference the window width within a JavaScript routine. For example, you might be executing a piece of code that changes the behaviour of a certain element in the page, but that element might only be displayed on large viewports anyway. In that case, it could be sensible to skip the execution completely if the window is smaller than a certain value.

.foo {
  display: none;

  // We're referencing the name of
  // the breakpoint, not the actual value
  @include media('>=large') {
    display: block;
  }
}
function doStuff() {
  // Bollocks, we're still referencing the value here!
  if (window.innerWidth < 1200) {
    return;
  }
    
  doHeavyStuff();
}

But there we are again — that “certain value” is one of the breakpoints we so proudly defined just once in our style sheets, but we’re now redeclaring its value since the name large won’t mean anything in JavaScript. So much for keeping things DRY.

To get around that, there’s a plugin called include-media-export, a combination of a SCSS routine that writes information about the breakpoints to the DOM, and a small JavaScript utility to read and process it. With the plugin, I can rewrite the function above without redeclaring the breakpoint, so all is good if someone decides to change its value down the line.

function doStuff() {
  // Yay, we're DRY again!
  if (im.lessThan('large')) {
    return;
  }
    
  doHeavyStuff();
}

The im object exposes the name of the current active breakpoint through im.getActive, while im.greaterThan and im.lessThan determine whether the window is wider or narrower than a certain breakpoint.

This is not a new concept, and many others wrote about it in the past (namely Les James and Mike Herchel). The idea here is to tie everything together in the same ecosystem, making the experience seamless for the developer.

There’s also layout

I don’t use any grid systems per se, but I find it convenient to have some classes purely for layout purposes. When applied to an element, these classes will define its width, instead of manually setting it on individual selectors — this keeps the style sheet organised and makes the markup a bit more semantic.

I used to end up with something like this:

.col--1-2 {
  width: 50%;
}

.col--1-3 {
  width: 33.3333%;
}

.col--2-3- { /* etc. */ }

After reading Harry Robert’s piece about BEMIT, I became really interested in his responsive suffixes approach. He suggests including a class with the name of a breakpoint to describe the state of an element at that screen size (e.g. .class-name@breakpoint). That idea led to include-media-columns, a plugin that generates column classes based on the breakpoints defined in include-media, following BEMIT’s naming convention.

Let’s take the list of breakpoints we defined before and imagine a user profile component that must:

  • Use the full width of its container on small viewports
  • Take half the width on medium viewports
  • Take a third of the width on large viewports
user-profile element changing based on the viewport

I can define that behaviour simply by giving the element the right classes in the HTML, without having to write any media queries or even touch CSS at all. It comes with the huge bonus of getting extremely semantic and meaningful markup.

<div class="user-profile col col--1-1 col--1-2@medium col--1-3@large">
  <!-- User profile -->
</div>

The suffix has been intentionally left out on col--1-1 as part of a mobile-first approach, making it the “default state” of the component. If you care about supporting old browsers and don’t want them to get the mobile view, the $im-media-support flag described in this post will get you sorted.

Making it part of the workflow

This is all nice and everything, but how easy is it to integrate this part of your workflow? It sort of defeats the purpose of trying to improve maintainability if this is a pain to install and update. Both include-media and the plugins are semver versioned and available as both Bower and NPM packages, so I simply include them as normal dependencies when I start a project.

$ npm install include-media include-media-export include-media-columns --save

Then I just import the SCSS files into my style sheet.

@import 'path/to/node_modules/include-media/dist/include-media';
@import 'path/to/node_modules/include-media-export/dist/include-media-export';
@import 'path/to/node_modules/include-media-columns/include-media-columns';

The Export plugin doesn’t require any additional configuration, whereas Columns requires you to specify how many subdivisions of the page you want to generate classes for.

// I want to be able to divide the page in halves, thirds and fifths
@include im-columns(2, 3, 5);

Wrapping up

All the breakpoints are now defined in a single centralised place, and any changes or additions will be propagated and made available to the scripts and to the layout — I think this leaves my DRY pet peeve at ease.

I’m not trying to sell you this workflow, and I’m certainly not claiming credit for any breakthrough idea. All this stuff existed before, but I wanted to give developers tools that tie everything together and makes development easier.

I can’t wait for element queries, CSS variables, CSS Grid Layout and other modern APIs to make everything described in this article completely obsolete — but until then, I’m happy with what a year (and 460 GitHub stars!) of include-media and its contributors have brought to my development workflow.