Managing Responsive Breakpoints with Sass

When dealing with responsive design, you often find yourself defining a lot of media queries, checking various properties, with various values. Because it can quickly get messy, it is usually considered a good practice to use Sass (or any other preprocessor, for that matter) to handle responsive breakpoints.

That being said, it has occurred to me there are lots of different ways to deal with this, and people keep asking me which one is the best. As with everything in our field, there is no straight answer and quite often, it depends. More precisely, I have noticed that the difficulty is not coming up with a system; it is coming up with a system that is both flexible enough to cover most cases yet not too complicated, which would result in code bloat.

In today’s article, I will walk you through a couple of ways to use Sass in order to manage responsive breakpoints. All are perfectly valid, but some are obviously better than others. That being said, I’ll let you make up your own opinion on this topic.

With variables

First, there is the way used by both Bootstrap and Foundation, which consists of defining variables, then using them in @media directives. In other words, you will have a configuration file somewhere containing a shitload of variables ready to be used as-is.

Here is how Bootstrap does it:

// Defining values
$screen-sm-min: 768px;
$screen-xs-max: ($screen-sm-min - 1);
$screen-md-min: 992px;
$screen-sm-max: ($screen-md-min - 1);
$screen-lg-min: 1200px;
$screen-md-max: ($screen-lg-min - 1);

// Usage
@media (max-width: $screen-xs-max) { ... }
@media (min-width: $screen-sm-min) { ... }
@media (max-width: $screen-sm-max) { ... }
@media (min-width: $screen-md-min) { ... }
@media (max-width: $screen-md-max) { ... }
@media (min-width: $screen-lg-min) { ... }

Foundation goes one step further, getting rid of the need of typing min-width and max-width by dealing with stringified media queries all together rather than pixel values.

// Defining values
$small-range:   (0em, 40em);       /* 0, 640px */
$medium-range:  (40.063em, 64em);  /* 641px, 1024px */
$large-range:   (64.063em, 90em);  /* 1025px, 1440px */
$xlarge-range:  (90.063em, 120em); /* 1441px, 1920px */
$xxlarge-range: (120.063em);       /* 1921px */

// Defining media queries
$screen:       "only screen" !default;
$landscape:    "#{$screen} and (orientation: landscape)" !default;
$portrait:     "#{$screen} and (orientation: portrait)" !default;
$small-up:     $screen !default;
$small-only:   "#{$screen} and (max-width: #{upper-bound($small-range)})" !default;
$medium-up:    "#{$screen} and (min-width:#{lower-bound($medium-range)})" !default;
$medium-only:  "#{$screen} and (min-width:#{lower-bound($medium-range)}) and (max-width:#{upper-bound($medium-range)})" !default;
$large-up:     "#{$screen} and (min-width:#{lower-bound($large-range)})" !default;
$large-only:   "#{$screen} and (min-width:#{lower-bound($large-range)}) and (max-width:#{upper-bound($large-range)})" !default;
$xlarge-up:    "#{$screen} and (min-width:#{lower-bound($xlarge-range)})" !default;
$xlarge-only:  "#{$screen} and (min-width:#{lower-bound($xlarge-range)}) and (max-width:#{upper-bound($xlarge-range)})" !default;
$xxlarge-up:   "#{$screen} and (min-width:#{lower-bound($xxlarge-range)})" !default;
$xxlarge-only: "#{$screen} and (min-width:#{lower-bound($xxlarge-range)}) and (max-width:#{upper-bound($xxlarge-range)})" !default;

// Usage
@media #{$small-up}     { ... }
@media #{$small-only}   { ... }
@media #{$medium-up}    { ... }
@media #{$medium-only}  { ... }
@media #{$large-up}     { ... }
@media #{$large-only}   { ... }
@media #{$xlarge-up}    { ... }
@media #{$xlarge-only}  { ... }
@media #{$xxlarge-up}   { ... }
@media #{$xxlarge-only} { ... }

There is one thing I don’t like with each method: in Bootstrap’s, I have to type (min-width: ...) every time. In Foundation’s, I need to interpolate a variable which is both ugly and annoying to do. Surely we could come up with a way to fix both flaws.

With a standalone mixin

I believe one of the most popular posts from CSS-Tricks is this one about media queries in Sass 3.2. In this article, Chris Coyier explains how he used a former idea by Mason Wendell which also used a former idea by Jeff Croft to name his responsive breakpoints with Sass.

Naming breakpoints is important because it gives sense to an abstract value. Do you always know what 767px refer to? I don’t. I’d rather know we are dealing with small screens. It is what both Bootstrap and Foundation started doing in storing media queries in variables; you know, variables are named.

So we could make a mixin that accepts a name (basically a string) as the only parameter, spitting out a media query. Right?

@mixin respond-to($breakpoint) {
  @if $breakpoint == "small" {
    @media (min-width: 767px) {
      @content;
    }
  }

  @else if $breakpoint == "medium" {
    @media (min-width: 992px) {
      @content;
    }
  }

  @else if $breakpoint == "large" {
    @media (min-width: 1200px) {
      @content;
    }
  }
}

Then, we can use it like this:

@include respond-to(small) { ... }
@include respond-to(medium) { ... }
@include respond-to(large) { ... }

This is actually nice, and for 2 reasons: not only does it make sense in itself, but it also centralizes all breakpoints in a single place: in the mixin core. If you ever have to change this 992px breakpoint to 970px, you don’t have to crawl through all your stylesheets; all you have to do is update the mixin and everything will work like a charm.

There are still two things not quite right with this mixin, however:

  1. Breakpoints cannot easily be pulled out of the mixin to a configuration file
  2. It is so redundant!

With a configurable mixin

In order to solve our two new flaws, we need to make some kind of list from our breakpoints. Then, this list can be moved around, dropped in a configuration file, left in the mixin core, whatever.

What would be very cool would be a way to map a name to a value. When using Sass 3.3+, there is a very simple way of doing so: maps. Introduced in Sass 3.3, maps do exactly that: they associate keys with values.

$breakpoints: (
  'small'  : 767px,
  'medium' : 992px,
  'large'  : 1200px
);

Neat! Now, we only have to tweak our previous mixin to retrieve values from the map instead of hardcoding everything:

@mixin respond-to($breakpoint) {
  // Retrieves the value from the key
  $value: map-get($breakpoints, $breakpoint);

  // If the key exists in the map
  @if $value != null {
    // Prints a media query based on the value
    @media (min-width: $value) {
      @content;
    }
  }

  // If the key doesn't exist in the map
  @else {
    @warn "Unfortunately, no value could be retrieved from `#{$breakpoint}`. "
        + "Please make sure it is defined in `$breakpoints` map.";
  }
}

Also note this cool little improvement: in case $breakpoint is not found in the breakpoints map, the user gets warned thanks to the @warn directive. This helps with debugging in case anything goes wrong.

Now, not only is our mixin very DRY, but it also handles errors quite well. Meanwhile, we removed one feature from our system: the ability to check for the property we want (min-width, max-width, max-height…). That being said if you use a mobile-first approach, this version should suit you quite well since you won’t need anything else than min-width media queries.

But if you want to control the type of media query to dump, you might want to add the feature back to our system. To do so, I recently came up with a rather elegant solution which doesn’t add any code complexity. Actually, it relies on the fact that Sass maps use the same syntax as CSS media queries (e.g. (property: value)).

[Maps] have no direct parallel in CSS, although they’re syntactically similar to media query expressions
Sass reference

$breakpoints: (
  'small'  : ( min-width:  767px ),
  'medium' : ( min-width:  992px ),
  'large'  : ( min-width: 1200px )
);
 
@mixin respond-to($name) {
  // If the key exists in the map
  @if map-has-key($breakpoints, $name) {
    // Prints a media query based on the value
    @media #{inspect(map-get($breakpoints, $name))} {
      @content;
    }
  }

  // If the key doesn't exist in the map
  @else {
    @warn "Unfortunately, no value could be retrieved from `#{$breakpoint}`. "
        + "Please make sure it is defined in `$breakpoints` map.";
  }
}

As you can see, this doesn’t make our mixin any longer. Actually, we even managed to refactor it to make it lighter! Among the changes, we needed a way to print a map because you may have noticed values from the breakpoints maps are maps as well as valid CSS media queries. Unfortunately, if you try to print a Sass map, you might encounter the following error:

( min-width: 767px ) isn’t a valid CSS value.

This is where the inspect function comes in handy. According to the Sass reference, it returns a string containing the value as its Sass representation. In other words, it is the only way to print a Sass map without making the compiler crash.

So to sum up what our mixin does:

  1. It checks whether the asked breakpoint exist in the breakpoints map
  2. If it exists, it prints a media query based on the value of the asked breakpoint
  3. If it doesn’t, it warns the user to check their code

Simple! If we look back at our previous solution’s flaws, we notice there is no more problem with WET (Write Everything Twice) code, or inflexible media queries. However there is one thing that this system doesn’t allow: complex media queries. By complex, I mean media queries involving several components (e.g. screen and (min-width: 767px)).

Because it relies on the fact that a simple CSS media query is a valid Sass map, it makes it very difficult to declare more complicated media queries. That being said, not only have I found it quite unlikely that we’ll need to use such queries, but our previous solutions (except the pure variables one from Bootstrap and Foundation) prevented us from doing so as well.

With an external tool

Last but not least, if you don’t feel like building your own mixin for whatever reason, you could use an existing tool to handle media query breakpoints. There are a couple of interesting Sass extensions that do the job well:

All three are top-notch Sass tools, so feel free to pick any of them. Here is a tiny comparative board if you find it difficult to pick one:

  SassMQ Breakpoint Breakup
MQ type *-width any any
No Query fallback yep yep yep
API complexity simple very simple medium
Code complexity very simple complexe simple
Extra Debug mode Singularity.gs

If I missed anything, or if there’s information that should be added, be sure to share.

SassMQ

// Configuration
$mq-responsive: true;
$mq-static-breakpoint: desktop;
$mq-breakpoints: (
  mobile:  320px,
  tablet:  740px,
  desktop: 980px,
  wide:    1300px
);

// Example
selector {
  @include mq($from: mobile) {
    property: value;
  }
}

Breakpoint

$high-tide: 500px;
$ex-presidents: 600px 800px;
$surfboard-width: max-width 1000px;
$surfboard-height: (min-height 1000px) (orientation portrait);

selector {
  @include breakpoint($high-tide) {
    property: value;
  }
}

Breakup

$breakup-breakpoints: (
  'thin' '(max-width: 35.999em)',
  'wide' '(min-width: 36em)',
  'full' '(min-width: 61em)'
);

selector {
  @include breakup-block('thin') {
    property: value;
  }
}

Final thoughts

As we have seen in this article, there are a lot of ways to deal with media query breakpoints. Each of them has its pros and cons, as there is no perfect system. In the end, I feel like it is up to you to decide what is a good balance between available features and code complexity.

It is always tempting to write a piece of code that covers every possible edge case, makes you coffee and does the laundry, too, but there is always a risk that in the end the tool is un-usable by anyone else but you. When working in a team, simplicity matters.

In any case, be sure to pick the right tool for the right job. ;)

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • adamcole

    I wanted to try this out but I am getting an error on:


    @media #{inspect(map-get($breakpoints, $name))}

    The error is:


    Invalid CSS after "": expected media query list, was "480px"

    • http://hugogiraudel.com/ Hugo Giraudel

      Please make sure you are using Sass 3.3. If it is not the problem, please provide an example showing your breakpoint map, the $name variable, and the code calling the mixin.

      • adamcole

        Yeah I’m using Sass 3.3, here’s my code:


        $breakpoints: (
        'xs' : $screen-xs, // 480px
        'sm' : $screen-sm, // 768px
        'md' : $screen-md, // 992px
        'lg' : $screen-lg // 1200px
        );

        @include respond-to('xs') {
        // Sass
        }

        • http://hugogiraudel.com/ Hugo Giraudel

          As I point out in the article, the final way is expecting a map like this `( min-width: 480px )`. Not a single length.

  • Codez

    Do you have a breakpoint for 480pm in your media query list?

    • http://hugogiraudel.com/ Hugo Giraudel

      What do you mean?

  • http://ramonlapenta.com/ Ramon Lapenta

    Very nice way to centralise media query values. Is there a way to also add new values on the fly? Like if you don’t have a value in your map and it’s only applicable to a specific component, I’m thinking something like @include respond-to($name, $new-breakpoint) {} and then use if it’s available.

  • Arnoud van Susteren

    Thank you for sharing.

    In your example I have omited the inspect() function and quoted the values defined under $breakpoints, this way I was able to write out more complex media queries as: ‘( min-width: 767) and ( max-width: 992px)’ . This way I also did not get the “Invalid CSS after” warning.

    See:
    http://sassmeister.com/gist/0614ac3971ae82925ef4

    • http://hugogiraudel.com/ Hugo Giraudel

      Definitely doable, for sure. :)

  • mindctrl

    This is really cool but it’s starting to feel a whole lot like full blown programming.

    • http://hugogiraudel.com/ Hugo Giraudel

      After all, why not? )

  • http://hugogiraudel.com/ Hugo Giraudel

    Yes, I think Sass maintainers are considering adding it, perhaps under a flag or something given the implications it could have.

    That being said, studies have been done ( http://sasscast.tumblr.com/post/38673939456/sass-and-media-queries ) and combining media queries would make very little difference at best and will mostly be completely insignificant. As long as we use gzip of course.

  • http://hugogiraudel.com/ Hugo Giraudel

    Would you mind putting your code on SassMeister/CodePen so I can have a look perhaps?

  • Wei

    Hello Hugo, thank you for sharing.

    I hope you wouldn’t mind if i ask is there any way that we can group the specified breakpoints @content together rather than having it compile the same breakpoints everytime the mixin is call? Cause i realise that the out having many repeated (e.g. @media (min-width: 1200px) { <> } )

    Thank you.

  • http://hugogiraudel.com/ Hugo Giraudel

    You’ll have to do it manually.