Conditional Media Query Mixins

Avatar of Chris Coyier
Chris Coyier on (Updated on )

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.

Other Approaches