Generate a separate css with flattened media queries #408

Closed
jakearchibald opened this Issue Jun 3, 2012 · 11 comments

Comments

Projects
None yet
4 participants
@jakearchibald

Mobile-first RWD is great, but it means serving IE<9 mobile styles, going slow with respond.js, or lumping all your media queries together and including them separately (eg http://adactio.com/journal/4494/), this means you've got overriding rules far away from the rules they're overriding, and you run into the same problems we had when we used to maintain a separate IE stylesheet included via conditional comments.

SASS could take a sass/scss/css file and output a file with the media queries either applied or flattened according to parameters. Eg:

/* Something that would take the following and a param of 900px... */

div {
    background-color: red;
}

@media screen and (min-width: 500px) {
    div {
        background-color: green;
    }
}

@media screen and (min-width: 1000px) {
    div {
        background-color: blue;
    }
}

/* ...and output... */

div {
    background-color: red;
}

div {
    background-color: green;
}

I'm not sure how this would work from the command line, perhaps...

sass --watch --flatten-mqs 900px -- stuff.scss

...would spit out 2 files, stuff.css would be as expected, stuff-no-mqs.css would be stuff.css but with media queries applied or removed as they would at a browser width of 900px. The developer would use conditional comments to avoid sending stuff.css to IE7/8, but send it stuff-no-mqs.css instead, giving it the styles other browsers get at a width of 900px. The developer may choose to serve stuff.css to IE6, resulting in those users getting basic mobile styles.

Basically, it lets the developer create a mobile-first stylesheet that falls back nicely in IE8 without dictating where they put their media query rules. Also, sites using respond.js could easily switch to this system and get the performance benefit of CDNs and no javascript dependency, without having to modify their CSS.

Unfortunately I don't have a lot of experience with Ruby, but I threw together a quick proof-of-concept in PhamtomJS https://github.com/jakearchibald/mq-apply/blob/master/mq-apply.js

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Jun 3, 2012

Really wanted to submit a pull request for this, but my Ruby skills are not up to it. I guess it would involve writing a simple tree visitor with a visit_media that removed media notes and recused their children if the expression matched

Really wanted to submit a pull request for this, but my Ruby skills are not up to it. I guess it would involve writing a simple tree visitor with a visit_media that removed media notes and recused their children if the expression matched

@chriseppstein

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jun 3, 2012

Member

This idea of generating multiple output files from a single input file comes up every once and a while and so far, we always come down on the side of favoring the simplicity that is derived from maintaining the correlation between a single input file to a single output file. This basic assumption that every css file has a corresponding sass file makes things more coherent for users and has numerous technical benefits.

In Sass 3.2 we have introduced @content block passing to mixins. With this feature you can roll this on your own -- which is how I accomplish this at my work.

First you define a mixin that can be configured to not output media queries when you've disabled them in a way that when the MQ are off you emit the block conditionally based on a fixed breakpoint:

$media-queries: true !default;
$media-query-free-breakpoint: 900px;
@mixin respond-to($min-width, $max-width: false) {
  @if $media-queries {
    @media screen and (min-width: $min-width) {
       @if $max-width {
         @media (max-width: $max-width) {
            @content
          }
       } @else {
         @content;
       }
     }
  }
  @else if $min-width <= $media-query-free-breakpoint and (not $max-width or $max-width and $max-width >= $media-query-free-breakpoint) {
    @content;
  }
}

Then you write your stylesheet as usual (let's say you call it screen.scss) and when you're ready to make your media query free version you make another file (let's call it screen-no-mqs.scss) and this file imports the first after disabling the MQs:

// screen.scss
@include respond-to(300px) {
  .this-is-in-ie { color: red; }
}
@include respond-to(1000px) {
  .this-is-not-in-ie { color: blue; }
}
@include respond-to(300px,500px) {
  .this-is-not-in-ie-either { color: green; }
}
@include respond-to(300px,900px) {
  .this-is-in-ie-too { color: yellow; }
}
// screen-no-mqs.scss
$media-queries: false;
@import "screen"

and you get the following output:

// screen.css
@media screen and (min-width: 300px) { .this-is-in-ie { color: red; } }
@media screen and (min-width: 1000px) { .this-is-not-in-ie { color: blue; } }
@media screen and (min-width: 300px) and (max-width: 500px) { .this-is-not-in-ie-either { color: green; } }
@media screen and (min-width: 300px) and (max-width: 900px) { .this-is-in-ie-too { color: yellow; } }
// screen-no-mqs.scss
.this-is-in-ie { color: red; }
.this-is-in-ie-too { color: yellow; }
Member

chriseppstein commented Jun 3, 2012

This idea of generating multiple output files from a single input file comes up every once and a while and so far, we always come down on the side of favoring the simplicity that is derived from maintaining the correlation between a single input file to a single output file. This basic assumption that every css file has a corresponding sass file makes things more coherent for users and has numerous technical benefits.

In Sass 3.2 we have introduced @content block passing to mixins. With this feature you can roll this on your own -- which is how I accomplish this at my work.

First you define a mixin that can be configured to not output media queries when you've disabled them in a way that when the MQ are off you emit the block conditionally based on a fixed breakpoint:

$media-queries: true !default;
$media-query-free-breakpoint: 900px;
@mixin respond-to($min-width, $max-width: false) {
  @if $media-queries {
    @media screen and (min-width: $min-width) {
       @if $max-width {
         @media (max-width: $max-width) {
            @content
          }
       } @else {
         @content;
       }
     }
  }
  @else if $min-width <= $media-query-free-breakpoint and (not $max-width or $max-width and $max-width >= $media-query-free-breakpoint) {
    @content;
  }
}

Then you write your stylesheet as usual (let's say you call it screen.scss) and when you're ready to make your media query free version you make another file (let's call it screen-no-mqs.scss) and this file imports the first after disabling the MQs:

// screen.scss
@include respond-to(300px) {
  .this-is-in-ie { color: red; }
}
@include respond-to(1000px) {
  .this-is-not-in-ie { color: blue; }
}
@include respond-to(300px,500px) {
  .this-is-not-in-ie-either { color: green; }
}
@include respond-to(300px,900px) {
  .this-is-in-ie-too { color: yellow; }
}
// screen-no-mqs.scss
$media-queries: false;
@import "screen"

and you get the following output:

// screen.css
@media screen and (min-width: 300px) { .this-is-in-ie { color: red; } }
@media screen and (min-width: 1000px) { .this-is-not-in-ie { color: blue; } }
@media screen and (min-width: 300px) and (max-width: 500px) { .this-is-not-in-ie-either { color: green; } }
@media screen and (min-width: 300px) and (max-width: 900px) { .this-is-in-ie-too { color: yellow; } }
// screen-no-mqs.scss
.this-is-in-ie { color: red; }
.this-is-in-ie-too { color: yellow; }
@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Jun 3, 2012

Interesting, which file has the mixin? If it's in screen.scss, won't $media-queries always be true?

Interesting, which file has the mixin? If it's in screen.scss, won't $media-queries always be true?

@chriseppstein

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jun 3, 2012

Member

Ah. sorry. I have updated that variable declaration to have a !default. So it will be true, unless it is already set to false.

Member

chriseppstein commented Jun 3, 2012

Ah. sorry. I have updated that variable declaration to have a !default. So it will be true, unless it is already set to false.

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Jun 3, 2012

That pretty much does the trick, obviously won't work on existing css files, but that's not a problem for me. Cheers!

That pretty much does the trick, obviously won't work on existing css files, but that's not a problem for me. Cheers!

@jakearchibald

This comment has been minimized.

Show comment
Hide comment

Demoed the technique over at http://jakearchibald.github.com/sass-ie/

@mhulse mhulse referenced this issue in jakearchibald/sass-ie Apr 17, 2013

Closed

LESS not compatible #3

@erfanimani

This comment has been minimized.

Show comment
Hide comment
@erfanimani

erfanimani May 6, 2013

This trick won't work if you have separate files for your media queries, which is quite common big projects and/or developing mobile first: "Import directives may not be used within control directives or mixins."

This trick won't work if you have separate files for your media queries, which is quite common big projects and/or developing mobile first: "Import directives may not be used within control directives or mixins."

@chriseppstein

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein May 7, 2013

Member

do the files for each media query contain the base styles (those that are not media specific)? If so, it should work.

Member

chriseppstein commented May 7, 2013

do the files for each media query contain the base styles (those that are not media specific)? If so, it should work.

@erfanimani

This comment has been minimized.

Show comment
Hide comment
@erfanimani

erfanimani May 7, 2013

Well basically, I have something like this in my screen.scss:

@import "base";

@media all and (min-width: 320px) {
  @import "responsive-320";
}

@media all and (min-width: 480px) {
  @import "responsive-480";
}

@media all and (min-width: 600px) {
  @import "responsive-600";
}

@media all and (min-width: 768px) {
  @import "responsive-768";
}

...

These imported files contain only media specific styles.

So in your respond-to mixin, we don't want to "print" @content, but instead import a file, which isn't allowed by sass..

I managed to work around this by wrapping all the media specific styles in a mixin, importing them all and then printing them with or without the media query.

Because SASS doesn't support interpolation of variable names, I had to create an if structure for each stylesheet. The following code works, but looks really really bad... I'm not sure if this could be refactored using sass map/hash data structures..

@import "responsive-320";
@import "responsive-480";
@import "responsive-600";
@import "responsive-768";
@import "responsive-940";

$media-queries: true !default;
$media-query-free-breakpoint: 940px;

/* Mobile */
@if $media-queries {
    @media all and (min-width: 320px) {
        @include responsive-320;
    }
}
@else if 320px <= $media-query-free-breakpoint {
    @include responsive-320;
}

/* Mobile */
@if $media-queries {
    @media all and (min-width: 480px) {
        @include responsive-480;
    }
}
@else if 480px <= $media-query-free-breakpoint {
    @include responsive-480;
}

/* HD Mobile / Tablet */
@if $media-queries {
    @media all and (min-width: 600px) {
        @include responsive-600;
    }
}
@else if 600px <= $media-query-free-breakpoint {
    @include responsive-600;
}

... 

Well basically, I have something like this in my screen.scss:

@import "base";

@media all and (min-width: 320px) {
  @import "responsive-320";
}

@media all and (min-width: 480px) {
  @import "responsive-480";
}

@media all and (min-width: 600px) {
  @import "responsive-600";
}

@media all and (min-width: 768px) {
  @import "responsive-768";
}

...

These imported files contain only media specific styles.

So in your respond-to mixin, we don't want to "print" @content, but instead import a file, which isn't allowed by sass..

I managed to work around this by wrapping all the media specific styles in a mixin, importing them all and then printing them with or without the media query.

Because SASS doesn't support interpolation of variable names, I had to create an if structure for each stylesheet. The following code works, but looks really really bad... I'm not sure if this could be refactored using sass map/hash data structures..

@import "responsive-320";
@import "responsive-480";
@import "responsive-600";
@import "responsive-768";
@import "responsive-940";

$media-queries: true !default;
$media-query-free-breakpoint: 940px;

/* Mobile */
@if $media-queries {
    @media all and (min-width: 320px) {
        @include responsive-320;
    }
}
@else if 320px <= $media-query-free-breakpoint {
    @include responsive-320;
}

/* Mobile */
@if $media-queries {
    @media all and (min-width: 480px) {
        @include responsive-480;
    }
}
@else if 480px <= $media-query-free-breakpoint {
    @include responsive-480;
}

/* HD Mobile / Tablet */
@if $media-queries {
    @media all and (min-width: 600px) {
        @include responsive-600;
    }
}
@else if 600px <= $media-query-free-breakpoint {
    @include responsive-600;
}

... 
@cimmanon

This comment has been minimized.

Show comment
Hide comment
@cimmanon

cimmanon May 7, 2013

I have the same problem as erfanimani, I want to import files with my "respond-to" mixin. It's the restriction placed on @import itself that's the problem.

@if true {
    @import "foo";
}

You get this error message: Import directives may not be used within control directives or mixins.

cimmanon commented May 7, 2013

I have the same problem as erfanimani, I want to import files with my "respond-to" mixin. It's the restriction placed on @import itself that's the problem.

@if true {
    @import "foo";
}

You get this error message: Import directives may not be used within control directives or mixins.

@chriseppstein

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein May 8, 2013

Member

@cimmanon @erfanimani Currently this limitation is created by our need to be able to efficiently compute dependencies for files that have not been compiled.

However, I do believe there is a way to have a more dynamically discovered dependency graph which would provide the same level of performance benefit for recompilation and allow us to remove the limitation for static analysis of @import directives.

Member

chriseppstein commented May 8, 2013

@cimmanon @erfanimani Currently this limitation is created by our need to be able to efficiently compute dependencies for files that have not been compiled.

However, I do believe there is a way to have a more dynamically discovered dependency graph which would provide the same level of performance benefit for recompilation and allow us to remove the limitation for static analysis of @import directives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment