Managing Responsive Breakpoints with Sass

Share this article

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. ;)

Frequently Asked Questions on Managing Responsive Breakpoints with SASS

How can I create a simple breakpoint with SASS?

Creating a simple breakpoint with SASS is straightforward. You start by defining a variable for your breakpoint. For instance, $tablet: 768px;. Then, you can use the @media directive to create a media query. Here’s an example:
$tablet: 768px;

@media (min-width: $tablet) {
// styles for tablet and above
}
In this example, the styles within the @media block will apply to devices with a screen width of 768px and above.

What is the best practice for organizing multiple breakpoints in SASS?

When dealing with multiple breakpoints, it’s best to use a map to store them. This way, you can easily manage and access your breakpoints. Here’s an example:
$breakpoints: (
'small': 576px,
'medium': 768px,
'large': 992px,
'xlarge': 1200px
);
You can then create a mixin to handle your media queries:
@mixin breakpoint($name) {
@media (min-width: map-get($breakpoints, $name)) {
@content;
}
}
And use it like this:
@include breakpoint('medium') {
// styles for medium and above
}

How can I create a breakpoint mixin that supports both min-width and max-width?

To create a breakpoint mixin that supports both min-width and max-width, you can modify the mixin to accept a second argument. Here’s an example:
@mixin breakpoint($min, $max: null) {
@if $max {
@media (min-width: $min) and (max-width: $max) {
@content;
}
} @else {
@media (min-width: $min) {
@content;
}
}
}
In this example, the $max argument is optional. If it’s provided, the mixin will create a media query that targets screen widths between $min and $max.

How can I use SASS to generate media queries for different devices?

You can use SASS to generate media queries for different devices by defining a map of device names and their corresponding screen widths. Then, you can create a mixin that generates the media queries. Here’s an example:
$devices: (
'phone': 480px,
'tablet': 768px,
'desktop': 1024px
);

@mixin device($name) {
@media (min-width: map-get($devices, $name)) {
@content;
}
}
You can then use the mixin like this:
@include device('tablet') {
// styles for tablet and above
}

How can I create a breakpoint mixin that supports multiple conditions?

To create a breakpoint mixin that supports multiple conditions, you can modify the mixin to accept a list of conditions. Here’s an example:
@mixin breakpoint($conditions) {
$query: '';
@each $condition in $conditions {
$query: $query + ' and (min-width: #{map-get($breakpoints, $condition)})';
}
@media #{unquote(trim($query, 3))} {
@content;
}
}
In this example, the mixin accepts a list of breakpoint names and generates a media query that targets screen widths that meet all the conditions.

Kitty GiraudelKitty Giraudel
View Author

Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/they.

Responsive Designsass
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week