Sass Mixin and Media Merging

Kitty Giraudel
Kitty Giraudel
Share

If you are not very familiar with Sass, you might not be aware that Sass enhances media queries (actually the @media directive) to provide extra interesting features. One of which is called (or rather often referred to as) media merging.

Before explaining to you what media merging is, did you know that the CSS specifications on Media Queries actually allow media queries to be nested? Some browsers like Firefox, Chrome and Opera support it; some other like Safari and Internet Explorer currently do not.

Because the browser support is not ideal, Sass kicks in and merges nested media queries into a single media condition. For instance, consider this code:

@media (min-width: 42em) {
  @media (max-width: 1337px) {
    .foo {
      color: red;
    }
  }
}

This will be compiled as:

@media (min-width: 42em) and (max-width: 1337px) {
  .foo {
    color: red;
  }
}

Pretty handy, isn’t it? This is what is called media merging. When nested media queries are merged into a single statement.

What do we want? Media queries!

Now that I have done with the introduction, let me get to the point. The other day, I was playing with this idea. Basically, I wanted to build a very simply mixin that takes a map of queries as input, and merge them into a single condition in a @media directive as an output.

Coming back to the previous example, I would like to write something like this:

@mixin media($queries) { .. }

.foo {
  @include media((min-width: 42em, max-width: 1337px)) {
    color: red;
  }
}

And when compiling, having the same result as seen in the CSS snippet above. Now, on the top of my head I can think of at least two ways of building it. Let’s tackle the ugly one first.

The ugly version

The most straight-forward idea would be to build a string out of our map. We iterate over the map, and for each new key/value pair, we concatenate them into the result string, separating pairs with the and keyword.

/// Media query merger (the ugly version)
/// Create a single media condition out of a map of queries
/// @param {Map} $queries - Map of media queries
@mixin media($queries) {
  $media-condition: ';

  // Loop over the key/value pairs in $queries
  @each $key, $value in $queries {
    // Create the current media query
    $media-query: '(' + $key + ': ' + $value + ')';

    // Append it to the media condition
    $media-condition: $media-condition + $media-query;

    // If pair is not the last in $queries, add a `and` keyword
    @if index(map-keys($queries), $key) != length($queries) {
      $media-condition: $media-condition + ' and ';
    }
  }

  // Output the content in the media condition
  @media #{$media} {
    @content;
  }
}

Okay, it’s not that ugly. It does the job fine, but you have to admit that this is not very elegant, is it?

The elegant way

I don’t feel very comfortable manipulating strings when Sass provides such an elegant way to to deal with media queries. There surely is a better way to do this. And then it stroke me: recursion. According to the free dictionary, recursion is:

A method of defining a sequence of objects, such as an expression, function, or set, where some number of initial objects are given and each successive object is defined in terms of the preceding objects. The Fibonacci sequence is defined by recursion.

That’s a bit tough. If we put this very simply, recursion is a mechanism where a function calls itself over and over again with different arguments until a certain point. A practical example of a function using recursion in JavaScript would be:

function factorial(num) {
  if (num < 0) return -1;
  else if (num == 0) return 1;
  else return (num * factorial(num - 1));
}

As you can see, the function calls itself until the num variable gets lesser than 1, by decreasing it by 1 at every run.

Why am I telling you this? I figured out we could use recursion to build our media condition using Sass media merging. What if we make the mixin output a media for the first query in the map, then call itself passing the map without this query, until there is no query left in the map? Let’s try it, and since it might be a bit complex, we’ll go step by step.

First, we now that if our map contains no (more) query, we simply output the content. What don’t we start with this?

@mixin media($queries) {
  @if length($queries) == 0 {
    @content;
  } @else {
    // ...
  }
}

Now, we want to output a media block for the first media query in the map. To get the first key of a map, we can use nth(..) and map-keys(..) functions.

$first-key: nth(map-keys($queries), 1);

@media ($first-key: map-get($queries, $first-key)) {
  // ...
}

So far, so good. Now, we only need to make the mixin call itself although we don’t want to pass it the same $queries map or we will face an infinite loop. We need to pass it $queries after having removed the first key/value pair. Thankfully, there is the map-remove(..) function for this.

$queries: map-remove($queries, $first-key);

@include media($queries) {
  @content;
}

Now the whole mixin:

/// Media query merger
/// Create a single media condition out of a map of queries
/// @param {Map} $queries - Map of media queries
@mixin media($queries) {
  @if length($queries) == 0 {
    @content;
  } @else {
    $first-key: nth(map-keys($queries), 1);

    @media ($first-key: map-get($queries, $first-key)) {
      $queries: map-remove($queries, $first-key);

      @include media($queries) {
        @content;
      }
    }
  }
}

Going further

In a previous article, we saw a couple of different ways to manage responsive breakpoints in Sass. The last version used a mixin that looks like this:

/// Breakpoints map
/// @type Map
$breakpoints: (
  'small': (min-width: 767px),
  'medium': (min-width: 992px),
  'large': (min-width: 1200px),
);

/// Responsive breakpoint manager
/// @param {String} $breakpoint - Breakpoint
/// @requires $breakpoints
@mixin respond-to($breakpoint) {
  $media: map-get($breakpoints, $breakpoint);

  @if not $media {
    @error "No query could be retrieved from `#{$breakpoint}`. "
    + "Please make sure it is defined in `$breakpoints` map.";
  }

  @media #{inspect($media)} {
    @content;
  }
}

This mixin works like a charm but it does not support multiple-queries conditions such as (min-width: 42em) and (max-width: 1337px) because it relies on the inspect(..) function which does nothing more than print the Sass representation of the value.

So, on one hand we have a breakpoint manager which picks from a global map of breakpoints and handle error messages and on the other with have a breakpoint manager which allow the use of multiple-queries conditions. The choice is hard.

Or is it?

By slightly tweaking the respond-to(..) mixin, we can make it include the media(..) mixin instead of printing a @media directive itself. Then, we have the best of both worlds.

@mixin respond-to($breakpoint) {
  // Get the query map for $breakpoints map
  $queries: map-get($breakpoints, $breakpoint);

  // If there is no query called $breakpoint in map, throw an error
  @if not $queries {
    @error "No value could be retrieved from `#{$breakpoint}`. "
    + "Please make sure it is defined in `$breakpoints` map.";
  }

  // Include the media mixin with $queries
  @include media($queries) {
    @content;
  }
}

The best thing is, if you already use this mixin, you can totally include the multi-queries feature by tweaking respond-to(..) and adding media(..) because the API does not change at all: respond-to(..) still needs a breakpoint name to work, the same as before.

Final thoughts

I must say I find this very exciting because it is the first time I have found a good use-case for both nested media queries and mixin recursion. While it is possible to skip this and simply build a string as we’ve seen with our first version, it sure is more elegant and interesting to tackle it with a recursive mixin. I hope you enjoyed it! The final example before leaving:

// _variables.scss
$breakpoints: (
  'small': (min-width: 767px),
  'small-portrait': (min-width: 767px, orientation: portrait),
  'medium': (min-width: 992px),
  'large': (min-width: 1200px),
);

// _mixins.scss
@mixin media($queries) { .. }
@mixin respond-to($breakpoint) { .. }

// _component.scss
.foo {
  @include respond-to('small-portrait') {
    color: red;
  }
}

Yielding the following CSS:

@media (min-width: 767px) and (orientation: portrait) {
  .foo {
    color: red;
  }
}

Frequently Asked Questions on Sass Mixin Media Merging

What is the main advantage of using Sass Mixin for media queries?

The primary advantage of using Sass Mixin for media queries is the ability to write DRY (Don’t Repeat Yourself) code. With Sass Mixin, you can define a group of CSS declarations that you want to reuse throughout your site. This way, you can avoid repeating the same code, making your stylesheets more maintainable and your workflow more efficient.

How does Sass Mixin differ from regular CSS media queries?

While both Sass Mixin and regular CSS media queries are used to apply different styles for different devices, Sass Mixin offers more flexibility and efficiency. With regular CSS media queries, you have to write the media query each time you want to apply a style. However, with Sass Mixin, you can define a Mixin once and then include it wherever you need it, reducing repetition and improving code readability.

Can I use Sass Mixin with other pre-processors like Less?

No, Sass Mixin is a feature specific to Sass, a CSS pre-processor. While Less, another popular CSS pre-processor, has similar features called Mixins, the syntax and usage may differ. Therefore, it’s important to understand the specific syntax and features of the pre-processor you’re using.

How can I use Sass Mixin for responsive design?

Sass Mixin can be incredibly useful for responsive design. You can define Mixins for different screen sizes and then include these Mixins in your stylesheets wherever you need them. This allows you to easily apply different styles for different devices, making your website responsive.

What is the syntax for defining a Mixin in Sass?

The syntax for defining a Mixin in Sass is quite straightforward. You use the @mixin directive followed by the name of the Mixin and then the styles you want to include. For example:
@mixin transform($property) {
-webkit-transform: $property;
-ms-transform: $property;
transform: $property;
}
You can then include this Mixin in your stylesheets using the @include directive.

Can I pass arguments to a Mixin in Sass?

Yes, you can pass arguments to a Mixin in Sass. This allows you to create more flexible and reusable styles. For example, you could define a Mixin for a border and then pass the border width and color as arguments.

How can I use Sass Mixin to merge media queries?

To merge media queries using Sass Mixin, you can define a Mixin for each media query and then include these Mixins in your stylesheets. Sass will then automatically merge all the styles under the same media query, resulting in cleaner and more efficient code.

What are the best practices for using Sass Mixin?

Some best practices for using Sass Mixin include keeping your Mixins small and focused, naming your Mixins clearly and descriptively, and using arguments to make your Mixins more flexible and reusable. It’s also a good idea to organize your Mixins in a separate file or files for better code organization.

Can I use Sass Mixin with CSS Grid or Flexbox?

Yes, you can use Sass Mixin with CSS Grid or Flexbox. You can define Mixins for different grid or flex properties and then include these Mixins in your stylesheets. This can make your code more maintainable and your workflow more efficient.

Are there any limitations or drawbacks to using Sass Mixin?

While Sass Mixin offers many benefits, it’s important to be aware of potential limitations or drawbacks. For example, overuse of Mixins can lead to bloated CSS output if not managed carefully. Also, since Mixins are a feature of Sass, they won’t work in regular CSS, so you’ll need to compile your Sass to CSS before deploying your stylesheets.