Skip to content

Instantly share code, notes, and snippets.

@calvinjuarez
Last active May 19, 2016 20:55
Show Gist options
  • Save calvinjuarez/ab166490c2433575e637bf0fb74e16b1 to your computer and use it in GitHub Desktop.
Save calvinjuarez/ab166490c2433575e637bf0fb74e16b1 to your computer and use it in GitHub Desktop.
Grouping Media Queries in Less with "Backwards Mixins"

Grouping Media Queries in Less with "Backwards Mixins"

backwards mixin: a mixin defined many times and called once, rather than defined once and called many times

This is a demo of one way you could use mixins to put all your media query rules together under a single @media statement. The merits of grouping media queries are dubious, but it's a good illustration of how to use backwards mixins.

How It Works

In Less, when a mixin is called it will output the results of all mixins that match the pattern established by the call.

For example, the Less in Figure 1-1 compiles to the CSS in Figure 1-2.

Figure 1-1:
// Less

.mixin (@switch; @color) {
  display: block;
}
.mixin (opaque; @color) {
  color: @color;
}
.mixin (faded; @color) {
  color: fade(@color, 50);
}

.class {
  .mixin(opaque; #acce55);
}
Figure 1-2:
/* CSS */

.class {
  display: block;
  color: #5ad;
}

In other words, Less calls all matching mixins at the point of a mixin call.

It's this behavior that we exploit to group all media queries of a given type under one @media block. This is demonstrated in 2.2-component.less & 2.3-media-queries.less. (The other files on the 2-level are just to show how this technique fits into a larger system.

Drawbacks

There are significand downsides, however. For example, in this system you can't nest the media queries in a rule. If you're doing a lot of nesting (like in a multi-level .nav, perhaps) you'd have to match all that nesting within the media query definition (see Figure 2).

But the major drawback is that you can't really use them inside other mixins, so complex libraries like Bootstrap (which uses mixin loops to generate its grid) wouldn't be able to use this.

Figure 2
// some-nav.less
.nav {
  // rules
  > li {
    // rules
    > a {
      // rules
    }
  }
}
.in-stylesheet(async) {
  .nav {
    // async rules ...
    > li {
      // async rules ...
      > a {
        // async rules ...
      }
    }
  }
}

Other Use Cases

Extracting Print Styles

I think this is probably the most useful use case. You use backwards mixins to extract print styles into their own file. See 3-print.less.

Extracting Asynchronous Styles

There are at least a few benefits to writing this way. Extrapolating from the print styles example, this could be used to extract styles not necessary for initial page load into a separate CSS file to be loaded asynchronously (see Figure 3); As you write your component's styles you can decide whether styles are necessary for page load.

Figure 3
// some-component.less

.some-component {
  color: gray;
  background-color: dodgerblue;
}
.in-stylesheet(async) {
  .some-component {
    transition: color .2s linear;
  }
}

...

// async.less

@import (reference) 'some-component.less';

.in-stylesheet(async) {}
& {
  .in-stylesheet(async);
}

Conclusion:

I can't personally think of a use case for large-scale sites: There's almost no performance benefit to grouping media queries; Print styles aren't usually complex and can probably be extracted automatically with a post-processor, and there have historically been bugs with Less's @import (reference) behavior when using :extend() in the referenced file; And there are many tools for automating detection and extraction of asynchronous "below-the-fold" styles already out there.

Overall, though, I feel like the principle—using mixins "backwards" by creating many definitions and calling it once—is powerfull. In smaller-scale sites this technique could save some time and headaches.

/*
* Media Query Grouping Demo Master Stylesheet
*/
@import '2.1-variables.less';
@import '2.2-component.less';
// etc.
@import '2.3-media-queries.less';
//
// Variables
//
// Breakpoints
// --------------------------------------------------
@breakpoint-sm: 600px;
@breakpoint-md: 1024px;
@breakpoint-lg: 1200px;
// Padding
// --------------------------------------------------
@padding-vertical-less: 10px;
@padding-vertical-more: 30px;
@padding-horizontal-less: 10px;
@padding-horizontal-more: 30px;
//
// Styles for Some Component
//
.component {
display: block;
padding-left: @padding-horizontal-less;
padding-right: @padding-horizontal-less;
background-color: #ace;
// etc.
}
.media(small-and-up) {
.component {
display: inline-block;
}
}
.media(medium-and-up) {
.component {
padding-left: @padding-horizontal-more;
padding-right: @padding-horizontal-more;
}
}
.media(print) {
.component {
display: block;
background-color: transparent;
}
}
//
// Media Queries
//
// The Mixins
// --------------------------------------------------
// defining all the .media mixins here avoids getting an undefined mixin error when compiling if a mixin hasn't yet been "used"
.media(zero-to-small) {}
.media(small-and-up) {}
.media(small-to-medium) {}
.media(medium-and-up) {}
.media(medium-to-large) {}
.media(large-and-up) {}
// The Actual Media Queries
// --------------------------------------------------
@media (max-width: (@breakpoint-sm - 1)) {
.media(zero-to-small); // calls all mixins defined that match .media(zero-to-small)
}
@media (min-width: @breakpoint-sm) {
.media(small-and-up); // calls all mixins defined that match .media(small-and-up)
@media (max-width: (@breakpoint-md - 1)) {
.media(small-to-medium); // calls all mixins defined that match .media(small-to-medium)
}
}
@media (min-width: @breakpoint-md) {
.media(medium-and-up); // calls all mixins defined that match .media(medium-and-up)
@media (max-width: (@breakpoint-lg - 1)) {
.media(medium-to-large); // calls all mixins defined that match .media(medium-to-large)
}
}
@media (min-width: @breakpoint-lg) {
.media(large-and-up); // calls all mixins defined that match .media(large-and-up)
}
/*
* Media Query Grouping Demo Extracted Print Stylesheet
*/
@import (reference) 'style.less'; // import--but don't output--all the main styles
.media(print) {} // fallback definition, in case no print styles were defined in style.less
@media print {
.media(print); // calls all mixins defined that match .media(print)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment