Grow your CSS skills. Land your dream job.

Conditional Media Query Mixins

Published by Chris Coyier

Sass makes working with media queries pretty excellent. You can nest them right within other blocks of CSS, which puts the properties and values you are changing right next to each other. That creates an obvious connection between them, which is a much nicer authoring experience than trying to maintain those changes separated by tons of other code or in a different file. We've covered this before. It was new in Sass 3.2.

.column-1-3 {
  width: 33.3333%;
  @media (max-width: 600px) {
    width: 100%;
  }
}

But since we're likely to do that over and over again, we should abstract that media query away like we would any other variable.

$bp-babybear: "(max-width: 600px)";

.column-1-3 {
  width: 33.33%;
  @media #{$bp-babybear} {
    width: 100%;
  }
}

It's more common to see a @mixin be used though, in conjunction with Sass @content blocks. We covered this in the article Naming Media Queries:

@mixin bp($point) {
  @if $point == papa-bear {
    @media (max-width: 1600px) { @content; }
  }
  @else if $point == mama-bear {
    @media (max-width: 1250px) { @content; }
  }
  @else if $point == baby-bear {
    @media (max-width: 600px)  { @content; }
  }
}

Which you use like this:

.sidebar {
  width: 33.33%;
  @include bp(baby-bear) {
    width: 100%;
  }
}

And there is no reason you can't do both, abstracting away the media queries into variables, if you prefer the look of that:

@mixin bp($point) {
  
  $bp-babybear: "(max-width: 600px)";
  $bp-mamabear: "(max-width: 1250px)";
  $bp-papabear: "(max-width: 1600px)";

  @if $point == papa-bear {
    @media #{$bp-papabear} { @content; }
  }
  @else if $point == mama-bear {
    @media #{$bp-mamabear} { @content; }
  }
  @else if $point == baby-bear {
    @media #{$bp-babybear}  { @content; }
  }

}

Which has the exact same usage.

As an aside, I like "bp" as a name, because it's something you have to type over and over and it's short and sweet.

At a glance, it seems like the @mixin approach has limited value. Just using a variable is less verbose and makes just as much sense. However with the @mixin, we are granted additional powers!

Perhaps we want to create a version of a stylesheet which contains no media queries at all. This is absolutely reasonable in some circumstances. Perhaps we want to create an IE 8 and down stylesheet. Nicolas Gallagher toyed with using Sass for this years ago, but this is slightly different. IE 8 and down do not natively support media queries so perhaps we decide we're going to serve them a stripped down stylesheet which does not include them at all. Old browsers like that are slower anyway, so instead of punishing them with additional code they can't use, we're helping them by serving them less. Sort of the Universal IE 6 Stylesheet idea.

We can use a combination of conditional comments to do that:

<!--[if !IE 8]><!-->
  <link rel="stylesheet" href="style.css">
<!--<![endif]-->

<!--[if gte IE 9]>
  <link rel="stylesheet" href="style.css">
<![endif]-->

<!--[if lte IE 8]>
  <link rel="stylesheet" href="style-NoMQs.css">
<![endif]-->

Another reasonable and real-world reason to serve a stylesheet stripped of media queries is if you have some pages of your site that use responsive design and some pages of your site that have a mobile-specific version. I think that's reasonable. The pages that have a mobile-specific version don't really need those media queries in them.

So how do you create both a style.css and a style-NoMQs.css from the same source Sass? We use the power of our @mixin.

We use a logic condition within the bp @mixin which will either continue and output the media query, or output nothing.

$MQs: true;

@mixin bp($point) {
  @if ($MQs) {
    $bp-babybear: "(max-width: 600px)";
    $bp-mamabear: "(max-width: 1250px)";
    $bp-papabear: "(max-width: 1600px)";
  
    @if $point == papa-bear {
      @media #{$bp-papabear} { @content; }
    }
    @else if $point == mama-bear {
      @media #{$bp-mamabear} { @content; }
    }
    @else if $point == baby-bear {
      @media #{$bp-babybear}  { @content; }
    }
  }
}

You wouldn't declare that variable right outside the @mixin though. You would declare it from your "master" file that @imports all the partials. So perhaps your style.scss file is:

$MQs: true;

@import "variables";
@import "colors";
@import "global";
/* etc. */

And you create the "No Media Queries" version (style-NoMQs.css) by creating a style-NoMQs.scss:

$MQs: false;

@import "variables";
@import "colors";
@import "global";
/* etc. */

Just change that variable to false, and the output CSS will contain no media queries at all.

Big props to Cat Farman who wrote about this very idea in her Cognition article Fall Back to the Cascade, with the slight variation of passing the support variable to the @mixin itself. (Is that safer?)

And as a final aside, I know many folks who don't like how Sass outputs repetitive media queries in the final output, only "bubbling out" the media query as far as the bottom of the nesting. That's necessary to honor the source-order-specificity of the selector (as I understand it). I've personally never worried about it since, since I always serve assets with GZip enabled, and GZip eats repetitive text for breakfast, it doesn't change the file size that much. Not to mention browsers have no problem zooming through and applying those rules. However if you still don't like that (or I'm wrong), there is a Grunt plugin for that.

Comments

  1. Awesome tip. I only wish there was a way to do inline MQs in SASS and still have them combine later.

  2. Permalink to comment#

    Hey Chris, this is great, it’s a great solution to still be able to build “mobile-first” websites but still be able to show the “desktop” version in IE8.

    I few months ago, I came up with a very similar solution, would love it if you could see what you think: https://github.com/peduarte/mobile-first-sass

    Cheers

  3. Chris Ruppel
    Permalink to comment#

    If you don’t want to maintain your own mixins, there is a Compass extension that automatically handles this (and a lot more!) called Breakpoint.

    I have covered other features in the comments of CSS Tricks before, but Breakpoint’s No Query fallbacks might be of interest to people who are reading this article.

    It has multiple output modes: separate stylesheet (as described by Chris Coyier in this post) and also a conditional class output method in the same stylesheet, in case you don’t want to incur a separate CSS download for the already slow, older IEs.

    For instance, you write some Sass:

    $bp-first = 642px, 'no-query' '.no-mq';
    
    .my-selector {
      background: red;
    
      @include breakpoint($bp-first) {
        background: blue;
      }
    }
    

    Generates this CSS:

    .my-selector {
      background: red;
    }
    @media (min-width: 642px) {
      .my-selector {
        background: blue;
      }
    }
    .no-mq .my-selector {
      background: blue;
    }
    
  4. Permalink to comment#

    A couple of months ago, before I had a Codepen account :’( , I was playing around with an idea to specify an ideal screen size for old IEs, instead of serving them the mobile or the full desktop version. This can be done for both widths and heights. Quick pen:

  5. This is exactly what the Guardian team does with their newly released sass-mq mixin. The theory is described in the README and in this article. It’s a neat solution, and easily grokked.

    I’ve been working on a small addition to set a target static breakpoint, so the static/rasterized output only contains styles that apply to a certain screen size, and avoids serving e.g. widescreen styles to IE8 and friends.

  6. Anders
    Permalink to comment#

    Thanks for sharing!

    I take a similar approach to mobile first development, but since I work with .less files, I don’t have access to @content blocks (at least not to my knowledge). So I had to come up with a solution that relies on media queries stored in variables. You would surely be able to implement this in SASS as well.

    In my master file there are several .less files being imported.:

    @import “variables.less”;
    @import “mixins.less”;
    @import “reset.less”;
    @import “base.less”;
    @import “layout.less”;
    @import “modules.less”;

    The media query variables are being stored in the variables.less file and read something like this:

    @mq-medium-and-up: ~”only screen and (min-width: @{breakpoint-medium})”;
    @mq-large-and-up: ~”only screen and (min-width: @{breakpoint-large})”;

    And this is an example (a bit contrived, I admit) of one of these variables in use:

    body {
    background: yellow;
    @media @mq-medium-and-up {
    background: red;
    }
    }

    I also keep a separate master file for IE <= 8 that looks something like this:

    @import “variables.less”;
    @mq-medium-and-up: ~”all”;
    @mq-large-and-up: ~”all”;
    @import “mixins.less”;
    @import “reset.less”;
    @import “base.less”;
    @import “layout.less”;
    @import “modules.less”;

    What happens above is that after the variables.less file has been imported, I immediately overwrite the media query variables and redefine them. Now, whenever I use these variables throughout my stylesheets they will be compiled into media rules, which are understood by older versions of Internet Explorer (not sure about IE < 7 though).:

    body {
    background: yellow;
    @media all {
    background: red;
    }
    }

    So basically these older browsers will get the desktop experience which I think could be a good alternative to leaving them with just the baseline experience. This should be a case by case decision obviously. Any thoughts on this solution?

  7. Ed Melly
    Permalink to comment#

    Great solution: I’ve used it in a couple of pretty large projects recently and it’s meant dealing with old IE a breeze. Jake Archibald came up with almost the exact same thing about a year ago: http://jakearchibald.github.io/sass-ie/

  8. Michael
    Permalink to comment#

    You can also put your variables into a list that the mixin loops through. I originally did this to keep from rewriting the mixin for each breakpoint and it seems to work pretty well. I don’t normally use the 3 bears analogy, but I changed it to be consistent with this article.

    $babybear: 30em !default;
    $mamabear: 48em !default;
    $papabear: 64em !default;
    
    $breakpoints: babybear $babybear, mamabear $mamabear, papabear $papabear !default;
    
    @mixin bp($point) {
      @each $breakpoint in $breakpoints {
        @if $point == nth($breakpoint, 1) {
          @media (min-width: nth($breakpoint, 2)) {
            @content;
          }
        }
      }
    }
    
    • msguerra74
      Permalink to comment#

      Also, to do a separate old IE stylesheet, you can do the following mixin:

      $babybear: 30em !default;
      $mamabear: 48em !default;
      $papabear: 64em !default;
      
      $breakpoints: babybear $babybear, mamabear $mamabear, papabear $papabear !default;
      
      $oldie: false !default;
      
      @mixin bp($point) {
        @if $oldie {
          @if ($point == babybear) or ($point == mamabear) {
            @content;
          }
        }
        @else {
          @each $breakpoint in $breakpoints {
            @if $point == nth($breakpoint, 1) {
              @media (min-width: nth($breakpoint, 2)) {
                @content;
              }
            }
          }
        }
      }
      

      Then, you can add the following variable to the top of your “oldie.scss” file:

      $oldie: true;
      

      And link your stylesheets to your HTML like this:

      <!--[if (gt IE 8) | (IEMobile)]><!-->
        <link rel="stylesheet" href="css/style.css">
      <!--<![endif]-->
      <!--[if (lt IE 9) & (!IEMobile)]>
        <link rel="stylesheet" href="css/oldie.css">
      <![endif]-->
      

      And write your sass like normal:

      .test {
        color: green;
      
        @include bp(mamabear) {
          color: blue;
        }
      
        @include bp(papabear) {
          color: red;
        }
      }
      

      Which would render in your normal “style.css” file as:

      .test {
        color: green;
      }
      
      @media (min-width: 48em) {
        .test {
          color: blue;
        }
      }
      
      @media (min-width: 64em) {
        .test {
          color: red;
        }
      }
      

      And render automatically to your “oldie.css” file without the media queries, using only the defined “oldie breakpoints” (babybear and mamabear) from the mixin as:

      .test {
        color: green;
        color: blue;
      }
      

      Whew, that was long, but it’s too cool not to try if you’re still wanting to support legacy IE browsers. Personally, I try to officially support IE8+, but like to make it look as good as it can in IE6/7 without spending too much time hacking away.

      Another thing you can do with the $oldie variable is something like this:

      .test {
        display: inline-block;
        @if $oldie {
          zoom: 1;
          *display: inline;
        }
      }
      

      Which will render to the normal “style.css” file as:

      .test {
        display: inline-block;
      }
      

      and to the “oldie.css” file as:

      .test {
        display: inline-block;
        zoom: 1;
        *display: inline;
      }
      

      Which will automatically put all of the IE hacks in the “oldie.css” file, keeping the “style.css” file clean. Ok, I’m done!

    • MarkupJunky
      Permalink to comment#

      Was just about to write the same – much better way!

  9. Jake
    Permalink to comment#

    This is a cool streamlined approach. However the only thing I hate about it is that your stylesheets end up littered with duplicate @media() declarations.

    It would be nice if SASS was smart enough to group the same media queries together like this:

    // SCSS
    .item1 {
        width: 50%;
        @media(max-width: 767px) {
            width: 100%;
        }
    }
    .item2 {
        width: 100%;
        @media(max-width: 767px) {
            width: 75%;
        }
    }
    // Output CSS
    .item1 { width: 50%; }
    .item2 { width: 100%; }
    @media(max-width: 767px) {
        .item1 { width: 100%; }
        .item2 { width: 75%; }
    }
    

    Instead it creates @media(max-width:767px)... blocks twice. At least it’s smart enough to pull the media query out of the style definition. So if you have a big style sheet with hundreds of elements that have different values for several different breakpoints you end up with literally thousands of @media() blocks added to your stylesheet in the end.

    I have experimented with adding breakpoint values to SASS lists via the append() command, and then iterating through the lists and generating your media queries all grouped together, but nested selectors makes it get messy real quick. Still looking for a solution on this.

    • @Jake 110% agree. +1.

      @Paul What if we don’t use Grunt…?

      I don’t see a solid reason to have to deal with yet another puzzle piece in an already complex and saturated development workflow just to be able to have Sass compile the media queries in an obvious way: at the bottom of the CSS.

      DRY out the window in this one.

      What Chris mentions at the end about GZipping and the browsers certainly helps “accepting” the issue, but not without leaving a bit of a sour taste.

    • @Eric, Thanks a lot for that article man, very informative.

      Impressed to see IE10 beat Chrome, and also how all IE8, IE9 and IE10 beat Chrome in the Microsoft.com tests. Truth be told, choosing Microsoft.com as one of the test sites seems like a very weird choice… IE8 and IE9 beating Chrome? C’mon :p

      But going back to the subject, my personal discontent with this whole “repetitive media queries in the final output” soap opera isn’t really performance, I kind of knew that in a way (and the numbers in this article corroborate that). Is more about the aesthetics of the final CSS file, especially when I think of students and other web designers/front end devs that are starting in this business since they won’t be able to see my pretty SCSS file(s) but only the final CSS file. I think is just plain hard to understand what’s really going on there. It may be so obvious to you and me but not necessarily to newcomers.

      Also, I kind of don’t like having to “abide” by Sass’ opinion on how I should compile my SCSS with media queries (I know we have several output styles: nested, expanded, compact and compressed, but this is a different subject). There should be a way, just like there is with the styles I just mentioned, to decide how the media queries are going to appear in the final CSS file after the compilation.

      If I was a programmer with Rails knowledge, I’d be trying to figure this one out a long time ago. But I am not. Fail.

      Regarding Grunt, I’ve been told that before and I’m sure that would be the case, but I haven’t seen a dumb-down, 3rd grader-type of tutorial on how to run the thing; I have tried several times to get that running but, believe me, but the tutorials I’ve seen assume you already know and understand many other things/concepts, and that’s where Grunt fails.

      I felt similarly about Sass some time back, until I was able to figure it out on my own, but it took me a while. Same thing happened to me not so long ago with the new Browser-Sync tool, until I was able to figure out on my own how to install it while the installation instructions weren’t clear and assummed “This guide assumes you already have Grunt installed inside a project and know how to use it.” Seriously. lol.

      And no, I didn’t install it using Grunt since it doesn’t need it ¬¬

  10. Permalink to comment#

    Nice post Chris, Ive been reading your blog posts for some time now – great work everytime.

  11. Dave
    Permalink to comment#

    I’ve just started playing around with Nicolas Gallaghers method. It’s ie8/mobile first. Only 1 style sheet is ever downloaded by the browser. It’s super, check it out!

    nicolasgallagher.com/mobile-first-css-sass-and-ie/

  12. GarethAWD
    Permalink to comment#

    I love the use of SASS, it’s going to save so much time!

  13. Jennifer Kyrnin
    Permalink to comment#

    Very nice. SASS is so handy and fun to use.

  14. Justin Perry
    Permalink to comment#

    Great post, Chris, as always. I did something fairly similar too. I used a linked list for the queries (will probably revert this to standard $variables at some point) and also included options to set a default view for non MQ devices and an option to include/exclude certain content.

    This article and some of the other comments have given me a few ideas which I might explore to extend the concept further, particularly in regards to the modular media queries and MQ buffering.

  15. Linus
    Permalink to comment#

    This technique works really well for retina as well!

    $isRetina: "(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 1.5)";
    
    @media #{$isRetina} {
    
    }
    

    Just a little quick tip ;-)

  16. The Conditional Comments way is the most used way to address IE’s “badmouthing”.

    How many go the “Paul Irish’s Conditional Comments in the <html> tag way” (PICCHTW) though?

    • Hand raised! I’m not very experienced with Sass yet, so I’m not sure how to make use of the html tag classes in this setup. I suppose something like Chris Ruppel’s solution could be used for this sort of thing? Like this?

      $bp-first = 642px, 'no-query' '.ie8';
      
      .my-selector {
        background: red;
      
        @include breakpoint($bp-first) {
          background: blue;
        }
      }
      

      To end up with this:

      .my-selector {
        background: red;
      }
      @media (min-width: 642px) {
        .my-selector {
          background: blue;
        }
      }
      .ie8 .my-selector {
        background: blue;
      }
      

      It doesn’t allow you to apply styles from media queries selectively, but it would allow you to feed a certain version of IE all the styles from a particular media query, so that it gets more than just the mobile styles.

      But like I said, I’m not very experienced with Sass, so someone please tell me if this is awful or just plain incorrect Sass. :-)

    • Chris Ruppel
      Permalink to comment#

      @Zoe yes :)

      I didn’t want to pin-point IE specifically in my example, but more often than not my .no-mq class is actually a specific conditional class like .lte-ie8

    • @Zoe I actually think Chris Ruppel’s mixin is a way to apply styles very selectively :) – BTW, I don’t see anything awful or wrong with your assessment, only a different point of view.

      I prefer going the “Pikachu” (PICCHTW) way with conditional classes rather than using (and managing) extra files for IE, and Chris Ruppel’s mixin just plain FTW.

  17. Ferdy
    Permalink to comment#

    Perhaps I’m the only one, but I strongly prefer to have a central set of media queries that each load their break-point specific SCSS file. The added benefit is that in a mobile-first setup, building for IE8 is simply the cumulative result of all styles. To each their own I guess.

  18. Cristian
    Permalink to comment#

    Mixing and variables for work with media queries

    // Usage
    @import "vendor/mediaquery";
    
    // SCSS usage
    @media screen #{$to-phone} { /* CSS here */ }
    
    // Or mixin usage:
    @include respond($to-phone) { /* CSS here */ }
    @include respond($from-phone) { /* CSS here */ }
    @include respond(to,320px) { /* CSS here */ }
    @include respond(from,321px) { /* CSS here */ }
    

    mediaquery.scss

    //  MEDIA QUERY VARIABLES AND MIXIN
    //  gist.github.com/cristianferrarig/4755293
    //
    //  @author  Cristian Ferrari
    //  @email   cristianferrarig@gmail.com
    //  @github  cristianferrarig
    //
    //  # Usage
    //  @import "vendor/mediaquery";
    //
    //  # SCSS usage:
    //  @media screen #{$to-phone} { /* CSS here */ }
    //
    //  # Or mixin usage:
    //  @include respond($to-phone) { /* CSS here */ }
    //  @include respond($from-phone) { /* CSS here */ }
    //  @include respond(#{$from-phone}#{$landscape}) { /* CSS here */ }
    //  @include respond(to,320px) { /* CSS here */ }
    //  @include respond(from,321px) { /* CSS here */ }
    //  @include respond($to-container) { /* CSS here */ }
    //
    
    @mixin respond($breakpoint,$size:null) {
      $mediaquery: $breakpoint;
    
      @if ($breakpoint == 'from' or $breakpoint == 'to') {
        @if $breakpoint == 'from' {
          $mediaquery: 'and (min-width: #{$size})';
        }
        @else if $breakpoint == 'to' {
          $mediaquery: 'and (max-width: #{$size})';
        }
      }
    
      @media screen #{$mediaquery} {
        @content;
      }
    }
    
    
    
    // Generic names
    // ---------------------------------------------------
    
    // Supported sizes
    $size-mini:           480px !default;
    $size-small:          768px !default;
    $size-medium:         992px !default;
    $size-large:          1200px !default;
    $size-oversized:      1500px !default;
    
    // To queries
    $to-mini:             'and (max-width: #{$size-mini})';
    $to-small:            'and (max-width: #{$size-small})';
    $to-medium:           'and (max-width: #{$size-medium})';
    $to-large:            'and (max-width: #{$size-large})';
    $to-oversized:        'and (max-width: #{$size-oversized})';
    
    // From queries
    $from-mini:           'and (min-width: #{1 + $size-mini})';
    $from-small:          'and (min-width: #{1 + $size-small})';
    $from-medium:         'and (min-width: #{1 + $size-medium})';
    $from-large:          'and (min-width: #{1 + $size-large})';
    $from-oversized:      'and (min-width: #{1 + $size-oversized})';
    
    // From/To queries
    $mini-to-small:       '#{$from-mini}   #{$to-small}';
    $small-to-medium:     '#{$from-small}  #{$to-medium}';
    $medium-to-large:     '#{$from-medium} #{$to-large}';
    $large-to-oversized:  '#{$from-large}  #{$to-oversized}';
    
    
    
    // Devices names
    // ---------------------------------------------------
    
    // Supported sizes
    $screen-phone:         $size-mini;
    $screen-tablet:        $size-small;
    $screen-laptop:        $size-medium;
    $screen-desktop:       $size-large;
    $screen-cinema:        $size-oversized;
    
    // To queries
    $to-phone:             'and (max-width: #{$screen-phone})';
    $to-tablet:            'and (max-width: #{$screen-tablet})';
    $to-laptop:            'and (max-width: #{$screen-laptop})';
    $to-desktop:           'and (max-width: #{$screen-desktop})';
    $to-cinema:            'and (max-width: #{$screen-cinema})';
    
    // From queries
    $from-phone:           'and (min-width: #{1 + $screen-phone})';
    $from-tablet:          'and (min-width: #{1 + $screen-tablet})';
    $from-laptop:          'and (min-width: #{1 + $screen-laptop})';
    $from-desktop:         'and (min-width: #{1 + $screen-desktop})';
    $from-cinema:          'and (min-width: #{1 + $screen-cinema})';
    
    // From/To queries
    $phone-to-tablet:      '#{$from-phone}  #{$to-tablet}';
    $tablet-to-laptop:     '#{$from-tablet} #{$to-laptop}';
    $laptop-to-desktop:    '#{$from-laptop} #{$to-desktop}';
    $desktop-to-cinema:    '#{$from-large}  #{$to-cinema}';
    
    
    
    // Others
    // ---------------------------------------------------
    
    // Container
    $size-container:        960px !default;
    $to-container:          'and (max-width: #{$size-container})';
    $from-container:        'and (min-width: #{1 + $size-container})';
    
    // Orientation
    $portrait:              'and (orientation:portrait)';
    $landscape:             'and (orientation:landscape)';
    
  19. Bhushan
    Permalink to comment#

    Super works :)

    I have put this to index
    ¨

    and to style.css

    @media screen and (min-width: 980px)
    {
    body{
    height:100%;
    }
    }

  20. Permalink to comment#

    I might be a bit late to the party, but when coding mobile-first AND supporting ie8, a very easy solution for me was:

    Mixin

    // Set IE Support
    $ie8-support: true;
    
    // Media Sizes
    $desktop: 960px;
    
    // Media Query Mixin
    @mixin media($screen) {
      @if $screen == desktop {
        @media (min-width: $desktop) {
          @content;
        }
        @if $ie8-support == true {
          .ie8 }
      }
    }
    

    Sass

    p {
      color: black;
    
      @include media(desktop) {
        color: blue;
      }
    }
    

    Output CSS

    p {
      color: black;
    }
    @media (min-width: 960px) {
      p {
        color: blue;
      }
    }
    .ie8 p {
      color: blue;
    }
    

    This is just a quick example, but as you see, if you set the $ie8-support variable to TRUE, it will add in the extra .ie8 class to the desktop media query. If you set it to FALSE, it doesn’t add anything. Works great.

    Codepen example:

    The idea of having 2 separate stylesheets just to support IE8 makes zero sense to me.

    Anyway… just my 2 cents.

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".