@media Queries & Breakpoints in SCSS

Published July 23, 2018

@media queries at multiple breakpoints are very common in a typical web project. It’s very easy to start tossing @media rules at the stylesheet but I find that scales poorly. I’ve personally been in the position of editing stylesheets with lots of @media rules all working with various breakpoints and it quickly becomes cumbersome to manage and grok what’s going on.

The point of this post isn’t to tell you what units or values you should use for your breakpoints, there’s plenty of other articles for that; I just want to discuss how I prefer to handle it in my SCSS1 stylesheets.

Breakpoints with Sass Maps

Sass maps make it easy to abstract our breakpoint values into key/value pairs:

$breakpoints: (
  xs: 250px,
  sm: 500px,
  md: 800px,
  lg: 1100px,
  xl: 1400px
);

Notice my naming convention: it’s purposely abstract and lacks any references to the values themselves or any device or resolution they may relate to. I prefer sticking to 2-character names. If I need to prepend a smaller value I would create an x2 key; likewise if I need to append a larger value I would create a 2x key. For more info on naming your breakpoints, check out Chris Coyier’s article on CSS-Tricks.

To reference an element from our map, we would use the map-get Sass builtin:

map-get($breakpoints, md); // 800px

Wrapping it in a @mixin

This is a bit unwieldly on it’s own, let’s create a mixin aptly named bp:

@mixin bp ($point, $direction: max) {
  @if map-has-key($breakpoints, $point) {
    $point: map-get($breakpoints, $point);
  }

  @media (#{$direction}-width: #{$point}) {
    @content;
  }
}

If you’re unfamiliar, Sass mixins are like typical functions except they return CSS instead of a specific value. This mixin’s logic is straightfoward: get the element out of map if it exists and generate the correct @media query. The @content directive allows us to pass a content block into the mixin, making for very simple usage. The beauty is that we can specify the min or max value in the @media query and use values that aren’t specified in our $breakpoints map — convenient for those one-offs.

The mixin could be used like so:

.foo {
  color: #f00;

  @include bp(sm) { // (max-width: 500px)
    color: #00f;
  }

  @include bp(1234px, min) { // (min-width: 1234px)
    color: #0f0;
  }
}

Honestly, sometimes that’s all you need. It’s still pretty minimal but gives much more flexibility & readability (both paramount to maintainable CSS). However, I’d like to expand it slightly more by dropping the 2nd parameter in favor of a more verbose 1st parameter:

@mixin bp ($expression) {
  $direction: str-slice($expression, 0, 1);
  $point: str-slice($expression, 2);

  @if map-has-key($breakpoints, $point) {
    $point: map-get($breakpoints, $point);
  }

  @if $direction == ">" {
    $direction: min;
  } @else if $direction == "<" {
    $direction: max;
  } @else {
    @error "Expected < or >, got: #{$direction}";
  }

  @media (#{$direction}-width: #{$point}) {
    @content;
  }
}

Nearly identical to our previous implementation with the major addition being an @if block checking the value of the first character on the input. Based on the first character, we determine the min or max value. If the first character is invalid, an @error is thrown. Yay control flow.

With this new mixin we can write succinctly:

.foo {
  color: #f00;

  @include bp("<sm") { // (max-width: 500px)
    color: #00f;
  }

  @include bp(">1234px") { // (min-width: 1234px)
    color: #0f0;
  }
}

Really, that’s all there is to it. Let’s recap:

  1. Create a map to hold your major breakpoints.
  2. Create a mixin to aid in writing your queries.

I think this is a great addition to any project and immensely aids in maintaining large CSS codebases, especially as the project iterates and changes ownership.


  1. I’m sure this technique is portable to other preprocessors.↩︎

Last modified January 31, 2019  #frontend   #dev   #sass 

🔗 Backlinks

← Newer post  •  Older post →