Sass mixin and media merging

Time:2021-7-7

If you are not familiar with sass, you may not know that sass has added many very interesting functions, such as the @ media function (often called media merging).

Sass mixin and media merging

Before explaining to you what media merging is, you should understand the basic principles of media queryCSS specificationWhether nested media queries are allowed. Some browsers support nested media queries, such as Firefox, chrome and opera; But Safari and Internet Explorer do not support nested media query at present.

Because browser support is not ideal, sass combines nested media queries into one media condition; For example, the following code:

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

Will be compiled as:

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

It’s very convenient. Is there any wood? When you merge nested media queries into a single statement, this is called media merge.

What do we want? Media queries!

Now that I’ve made a brief introduction, let me talk about my idea: basically, I want to build a very simple mixin, which takes the mapping of the query as the input and merges them into a single condition in the @ Meida instruction as the output.

Going back to the previous example, I would like to write as follows:

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

At compile time, the result is the same as that in the CSS code above. In my opinion, I can create it in at least two ways. Let’s solve the ugly one first.

Ugly version

The most direct idea is to build a string, and we iterate over each new key value pair

    /// 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;
      }
    }

Although its functions are well implemented, you have to admit that such code is not very elegant.

Elegant way

When sass provides such an elegant way to handle media queries, I don’t feel comfortable manipulating strings. There must be a better way to do that. Then he inspired me: recursion. According to the dictionary, recursion means:

A method of defining a sequence of objects (such as expressions, functions, or sets) in which some number of initial objects are given and each successive object is defined according to the previous object.

If we only understand recursion in this simple way, recursion is a function that calls its own mechanism over and over again with different parameters until a certain point. So it’s a little hard. A practical example of using recursive functions in javascript:

    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 is less than 1, decreasing by 1 per run.

Why should I tell you this? I think we can use recursion to build our media conditions and sass to merge media. If we export mixin to the first query medium in map, then call itself to pass map until there is no query in map. Because he is a bit complicated, we have to go step by step.

First of all, if our map doesn’t have more queries, we just output the content.

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

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

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

So far, it’s good. Now, we just need to use mixin to call ourselves. We don’t want to pass him the same $queries, otherwise we will face an infinite loop. We need to pass $query after deleting the first key / value pair. Fortunately, there is also a map remove (..) function.

    $queries: map-remove($queries, $first-key);
    
    @include media($queries) {
      @content;
    }

Now the whole mixin is like this:

    /// 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;
          }
        }
      }
    }

further more

stayPrevious articleIn, we see several different ways to manage response breakpoints in sass. The last version used in mixin is as follows:

    /// 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 is like a char, but it does not support multi condition queries, such as (min width: 42em) and (max width: 1337px), because it relies on the inspect (..) function and only prints the value represented by sass.

Therefore, on the one hand, we have a breakpoint manager that selects and processes error messages from the global map of breakpoints, and on the other hand, we have a breakpoint manager that allows multiple query conditions. The choice is difficult.

By adjusting the response to (..) mixin slightly, we can make it include media (..) mixin instead of printing a @ media instruction itself.

    @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 way is that if you have used the mixin, you can adjust the response to (..) and add media (..) to complete the multi query function, because the API has not changed at all: as before, the response to (..) still needs a breakpoint name to work.

Last thought

I have to say, I find this very exciting, because this is the first time I’ve found a good example of nested media queries and mixin recursion. Last example:

    // _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;
      }
    }

The following CSS is generated:

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


Author information

Original author: Hugo giraudel
Link to the original text:https://www.sitepoint.com/sas…
Translated fromMaxLeapTeam_ Front end R & D personnel: ammie Bai
Translator profile: a new front-end, currently responsible for the implementation of maxleap website display content. Like to try to write some JS special effects demo.

Related articles
Image clipping without flash

The author’s best works in the past
Introduction to jcanvas: when jQuery meets HTML5 canvas
How to use postcss with gulp

Want to know about app production and development? Welcome to join QQ group: 480843919