Extends in MediaQueries #640

Closed
jakob-e opened this Issue Jan 25, 2013 · 9 comments

Projects

None yet

5 participants

@jakob-e
jakob-e commented Jan 25, 2013

@extends are great - but why they can't be used in @media I don't understand ???
It becomes a BIG issue when trying to create reusables or frameworks.

Also it seems odd that while you can't use an @extend placed outside @media it
will mess up the output if named the same inside (test 2 +3)

Thanks for making things a lot better :)
Jakob E (using v. 3.2.5 + CodeKit)


  1. How it ought to work - but dosn't :(
// SCSS
%span3 { float:left; width:100%; margin-right:0; }
%span2 { float:left; width:65.9575%; margin-right:2.1277%; }
%span1 { float:left; width:31.9149%; margin-right:2.1277%; }

.foo  { @extend %span1; }
.bar  { @extend %span2; }
.doh  { @extend %span3; }
.taz  { @extend %span3; }

@media only screen and (max-width: 480px) {
    .foo { @extend %span3; }
    .bar { @extend %span3; }
    .doh { @extend %span2; }
    .taz { @extend %span1; }        
}
// OUTPUT: 
.doh, .taz { float: left; width: 100%; margin-right:0; }
.bar { float: left; width: 65.9575%; margin-right: 2.1277%; }
.foo { float: left; width: 31.9149%; margin-right: 2.1277%; }

 @media only screen and (max-width: 480px) { 
    .foo, .bar { float: left; width: 100%; margin-right:0;  }
    .doh { float: left; width: 65.9575%; margin-right: 2.1277%; }
    .taz { float: left; width: 31.9149%; margin-right: 2.1277%; } 
 }

  1. Version 3.2.3 allows extends to be placed within - so let's do:
// SCSS    
%span3 { float:left; width:100%; margin-right:0; where: Outside; }
%span2 { float:left; width:65.9575%; margin-right:2.1277%; where: Outside;}
%span1 { float:left; width:31.9149%; margin-right:2.1277%; where: Outside;}

.foo  { @extend %span1; }
.bar  { @extend %span2; }
.doh  { @extend %span3; }
.taz  { @extend %span3; }

@media only screen and (max-width: 480px) {
     %span3 { float:left; width:100%; margin-right:0; where: Inside; }
     %span2 { float:left; width:65.9575%; margin-right:2.1277%; where: Inside;}
     %span1 { float:left; width:31.9149%; margin-right:2.1277%; where: Inside;}     
    .foo { @extend %span3; }
    .bar { @extend %span3; }
    .doh { @extend %span2; }
    .taz { @extend %span1; }        
} 
 // OUTPUT:
 // As expected 
 .doh, .taz { float: left; width: 100%; margin-right:0; where: Outside; }
 .bar { float: left; width: 65.9575%; margin-right: 2.1277%; where: Outside; }
 .foo { float: left; width: 31.9149%; margin-right: 2.1277%; where: Outside; }

 @media only screen and (max-width: 480px) { 
     // THIS IS SO WRONG !
     .doh, .taz, .foo, .bar { float: left; width: 100%; margin-right:0; where: Inside; }
     .bar, .doh { float: left; width: 65.9575%; margin-right: 2.1277%; where: Inside; }
     .foo, .taz { float: left; width: 31.9149%; margin-right: 2.1277%; where: Inside; } 
 }

 // EXPECTED OUTPUT: 
 .doh, .taz { float: left; width: 100%; margin-right:0; where: Outside; }
 .bar { float: left; width: 65.9575%; margin-right: 2.1277%; where: Outside; }
 .foo { float: left; width: 31.9149%; margin-right: 2.1277%; where: Outside; }

 @media only screen and (max-width: 480px) { 
     .foo, .bar { float: left; width: 100%; margin-right:0; where: Inside; }
     .doh { float: left; width: 65.9575%; margin-right: 2.1277%; where: Inside; }
     .taz { float: left; width: 31.9149%; margin-right: 2.1277%; where: Inside; } 
 }

  1. OK maybe it has something to do with the naming - let's add a and b:
// SCSS
%span3a { float:left; width:100%; margin-right:0; where:Outside; }
%span2a { float:left; width:65.9575%; margin-right:2.1277%; where: Outside;}
%span1a { float:left; width:31.9149%; margin-right:2.1277%; where: Outside;}

.foo  { @extend %span1a; }
.bar  { @extend %span2a; }
.doh  { @extend %span3a; }
.taz  { @extend %span3a; }

@media only screen and (max-width: 480px) {
    %span3b { float:left; width:100%; margin-right:0; where:Inside; }
    %span2b { float:left; width:65.9575%; margin-right:2.1277%; where: Inside;}
    %span1b { float:left; width:31.9149%; margin-right:2.1277%; where: Inside;}     
    .foo { @extend %span3b; }
    .bar { @extend %span3b; }
    .doh { @extend %span2b; }
    .taz { @extend %span1b; }        
} 
// OUTPUT - NOW AS EXPECTED:
.doh, .taz { float: left; width: 100%; margin-right:0; where: Outside; }
.bar { float: left; width: 65.9575%; margin-right: 2.1277%; where: Outside; }
.foo { float: left; width: 31.9149%; margin-right: 2.1277%; where: Outside; }

 @media only screen and (max-width: 480px) { 
    .foo, .bar { float: left; width: 100%; margin-right:0; where: Inside; }
    .doh { float: left; width: 65.9575%; margin-right: 2.1277%; where: Inside; }
    .taz { float: left; width: 31.9149%; margin-right: 2.1277%; where: Inside; } 
 }
@robwierzbowski
Contributor

If you search 'media extend' there are a ton of issues that talk about this. At its most basic, @extend adds the selector to a list where the placeholder was defined. The short answer why this won't work is that the following isn't valid css:

.this-style, .that-style, @media (min-width: 25em) { .other_style }, .last-style {
  ...
}
@jakob-e
jakob-e commented Jan 28, 2013

That explains how the compiler works - thanks

But as I see it - if the @media rule were to:

  • create a nested placeholder list, referencing the same css values as the original
  • restricting append to nested elements
  • denying referral of nested lists

... wouldn't we be happy ? :)

Best,
Jakob E

@nex3
Contributor
nex3 commented Feb 2, 2013

As @robwierzbowski explains, @extend is all about moving selectors around -- not CSS rules. In order to support cross-@media extend, actual CSS rules would have to be copied and moved, which is unexpected and contrary to the way @extend behaves in every other circumstances. In addition, it can cause unexpected changes in specificity to the CSS output.

If copying the CSS is the behavior you want, we recommend you use mixins for that, as they're the language construct that explicitly supports copying.

@nex3 nex3 closed this Feb 2, 2013
@BPScott BPScott referenced this issue in csswizardry/csswizardry-grids Mar 25, 2013
Open

responsive grid classes #15

@lunelson

I think the problem here is that while we can't extend an outer class from within a media query, inner classes (inside a query or nest) are extended along with outer ones when an @extend is applied in an outer context. So while we can't extend freely between contexts, we have no way to keep them completely separate either (other than namespacing the classes we want to extend), which makes @extend more trouble than its worth.

In my view, @extend needs something like a strict mode, which matches only the same selector both in terms of context and in nesting, that would allow us to treat these things as different scopes, and know exactly what was going to extend what.

This is incidentally how the "Roole" preprocessor has chosen to implement @extend (strict matching, including nested selectors), and they are experimenting with an @extend-all directive which would match more broadly.

http://roole.org/#extend

@jakob-e
jakob-e commented Apr 17, 2013

Extends in MediaQueries - a sort of workaround

Knowing that we can extend classes inside MediaQueries as long as we make sure the class names does not conflict (prefix/namespace) - then all we need to do is to create a media version of each class.

// This will work
@media (max-width:480px){  %mobile_class{ content:'Mobile'; };  }
@media (min-width:481px) and (max-width:768px){  %tablet_class{ content:'Tablet'; }  }

.foo { @extend %mobile_class; }
.foo { @extend %tablet_class; }
.bar { @extend %mobile_class; }
.bar { @extend %tablet_class; } 

// Output:
@media (max-width: 480px) { .foo, .bar { content: 'Mobile'; } }
@media (min-width: 481px) and (max-width: 768px) { .foo, .bar { content: 'Tablet'; } }

OK - let's get some automation
.... please comment :)

// ====================================================================
// MediaExtends  (very much a beta version)
//  
//  First - let's start by defining the breakpoints we need 
//  (key-label, min-value, max-value). 0 = omitted.
$breakpoints:(
    (mobile,0,480),
    (tablet,481,768),
    (desktop,769,1200),
    (large,1201,0)
); 

//  Next - a function to get breakpoints by key
//  (objects would have been nice ;)
@function breakpoint($key){
    @for $i from 1 through length($breakpoints){ 
        @if(nth(nth($breakpoints,$i),1)==$key){ @return nth($breakpoints,$i); }  
    } @return $key;
}

//  Next - a mixin to create prefixed versions of the classes
//  we want to extend within @media.
//  
//  Example:
//  @include mediaExtend(myExtend){ @content };
//
//  Output:
//  %mobile_myExtend  { @content } 
//  %tablet_myExtend  { @content }
//  %desktop_myExtend { @content }
//  %large_myExtend   { @content }
//
//  passing true as second parameter will
//  create a regular extend class too
// 
//  E.g.   %myExtend  { @content }
@mixin mediaExtend($name,$regularextend:false){
    @if($regularextend){ %#{$name}{ @content; }; }
    @for $i from 1 through length($breakpoints){
        $key:nth(nth($breakpoints,$i),1);
        %#{$key+'_'+$name}{ @include media($key){ @content; }  };
    }
}
// Last - a mixin to create MediaQueries
// $breakpoint - $breakpoints key-label e.g. mobile, tablet,...
// $extends... - mediaExtends to extend (no prefix) 
//
// ------------------------------------ 
// Example 1 - basic:  
// ------------------------------------
// @include mediaExtend(myExtend){ border:1px solid #f0f; };
// .foo { @include media(mobile,myExtend); }
// 
// Output: 
// @media screen and (max-width: 480px) { 
//  .foo { border:1px solid magenta; } 
// }
//
// ------------------------------------
// Example 2 - multible includes:
// ------------------------------------
// @include mediaExtend(myExtend){ border:1px solid #f0f; };
// .foo { @include media(mobile,myExtend); } 
// .bar { @include media(mobile,myExtend); } 
//
// @media screen and (max-width: 480px) { 
//  .foo, .bar {  border: 1px solid magenta; }
// }
// 
// ------------------------------------
// Example 3 - multible extends:
// ------------------------------------
// @include mediaExtend(myExtend_1){ border:1px solid #f0f; };
// @include mediaExtend(myExtend_2){ color:red; };
// .foo { @include media(mobile, myExtend_1, myExtend_2); }
// 
// @media screen and (max-width: 480px) { 
//  .foo { border: 1px solid magenta; } 
// }
// @media screen and (max-width: 480px) { 
//  .foo { color: red; } 
// }
//
// I'm still looking for a way to store @content 
// to provide an optimized version - like:;
// @media screen and (max-width: 480px) { 
//   .foo { border: 1px solid magenta; color: red;}
// }
// - Please let me know if you know :)
// 
// ...or we'll just have to wait for 
// https://github.com/nex3/sass/issues/116
// 
// ------------------------------------
// Example 4 - @content:
// ------------------------------------
// @include mediaExtend(myExtend){ border:1px solid #f0f; };
// .foo { @include media(mobile,myExtend){ color:red; }; } 
// .bar { @include media(mobile,myExtend){ color:blue; }; } 
//
// Output:
// @media screen and (max-width: 480px) { 
//  .foo, .bar { border: 1px solid magenta; } 
// }
// @media screen and (max-width: 480px) { 
//  .foo { color: red; } 
// }
// @media screen and (max-width: 480px) { 
//  .bar { color: blue; } 
// }
// 
@mixin media($breakpoint,$mediaextends...){
    $min:nth(breakpoint($breakpoint),2)* 1px; 
    $max:nth(breakpoint($breakpoint),3) * 1px; 
    $mq:'(min-width:'+$min+') and (max-width:'+$max+')';
    @if($min and $max==0px){ $mq:'(min-width:'+$min+')'; }
    @if($min==0px and $max){ $mq:'(max-width:'+$max+')'; }
    @each $ext in $mediaextends { @extend #{'%'+$breakpoint +'_'+ $ext }; }
    @media screen and #{unquote($mq)}{ @content; }
}

The shorter version

breakpoints:(
    (mobile,0,480),
    (tablet,481,768),
    (desktop,769,1200),
    (large,1201,0)
); 
@function breakpoint($key){
    @for $i from 1 through length($breakpoints){ 
        @if(nth(nth($breakpoints,$i),1)==$key){ @return nth($breakpoints,$i); }  
    } @return $key;
}
@mixin mediaExtend($name,$regularextend:false){
    @if($regularextend){ %#{$name}{ @content; }; }
    @for $i from 1 through length($breakpoints){
        $key:nth(nth($breakpoints,$i),1);
        %#{$key+'_'+$name}{ @include media($key){ @content; }  };
    }
}
@mixin media($breakpoint,$mediaextends...){
    $min:nth(breakpoint($breakpoint),2)* 1px; 
    $max:nth(breakpoint($breakpoint),3) * 1px; 
    $mq:'(min-width:'+$min+') and (max-width:'+$max+')';
    @if($min and $max==0px){ $mq:'(min-width:'+$min+')'; }
    @if($min==0px and $max){ $mq:'(max-width:'+$max+')'; }
    @each $ext in $mediaextends { @extend #{'%'+$breakpoint +'_'+ $ext }; }
    @media screen and #{unquote($mq)}{ @content; }
}


// Example: 
// A super simple responsive grid
@mixin Grid($grid-columns){
  $i:1;
  @while $i<=$grid-columns {
    @include mediaExtend(gridSpan#{$i},true){ 
        float:left;        
        width:100% / $grid-columns * $i;
    }
    $i:$i+1;
  }
}
@include Grid(4);
// Does the same as: 
// @include mediaExtend(gridSpan1,true){ float:left; width:25%;  }
// @include mediaExtend(gridSpan2,true){ float:left; width:50%;  }
// @include mediaExtend(gridSpan3,true){ float:left; width:75%;  }
// @include mediaExtend(gridSpan4,true){ float:left; width:100%; }

// Just plain stupid ;)
@include mediaExtend(comment){ &:after { content:'Hey there!'; };  }

article {
    @extend %gridSpan1;
    @include media(mobile,  gridSpan4){ color:blue; };
    @include media(tablet,  gridSpan3);
    @include media(desktop, gridSpan2);
    @include media(large,   gridSpan1, comment);
}
section {
    @extend %gridSpan2;
    @include media(mobile, gridSpan4){ color:red; };
    @include media(tablet, gridSpan3);
}


// Output:
article { float: left; width: 25%; }
@media screen and (min-width: 1201px) { article { float: left; width: 25%; } }
section { float: left; width: 50%; }
@media screen and (min-width: 769px) and (max-width: 1200px) { article { float: left; width: 50%; } }
@media screen and (min-width: 481px) and (max-width: 768px) { article, section { float: left; width: 75%; } }
@media screen and (max-width: 480px) { article, section { float: left; width: 100%; } }
@media screen and (min-width: 1201px) { article:after { content: 'Hey there!'; } }
@media screen and (max-width: 480px) { article { color: blue; } }
@media screen and (max-width: 480px) { section { color: red; } }


Happy SCSS'ing
Jakob E

@lunelson

That is an epic workaround!

IMO it seems like a Pandora's box that's been opened up with the features in 3.2 (but a good one); I'm convinced it must means however that more control or specificity is needed with respect to @extend across different contexts. Maybe a !local or !strict flag after @extend declarations to keep from picking up nested or media-scoped instances—that would save a lot of trouble—anyway still thinking, I plan to post a separate issue on it

Awesome work with that approach above

@cimmanon

In order to support cross-@media extend, actual CSS rules would have to be copied and moved, which is unexpected and contrary to the way @extend behaves in every other circumstances. In addition, it can cause unexpected changes in specificity to the CSS output.

I don't buy this one bit. "Why can't I @extend within using media queries" is a question that pops up on SO every now and then (in addition to the number of issues that are regularly opened here), so I would say the opposite is true: the fact that the styles aren't copied over is the unexpected behavior.

If copying the CSS is the behavior you want, we recommend you use mixins for that, as they're the language construct that explicitly supports copying.

Until we get mixin interpolation or the ability to pass a mixin as an argument to another mixin, this isn't a reasonable solution in some instances. I get that you don't want to change how @extend behaves, so what about adding a @copy construct?

@nex3
Contributor
nex3 commented Apr 17, 2013

I think the problem here is that while we can't extend an outer class from within a media query, inner classes (inside a query or nest) are extended along with outer ones when an @extend is applied in an outer context. So while we can't extend freely between contexts, we have no way to keep them completely separate either (other than namespacing the classes we want to extend), which makes @extend more trouble than its worth.

This isn't a problem; this is exactly how @extend is supposed to work. When you're extending a selector, you're saying that the extender should be styled as though it matches the extendee. If .serious-error extends .error, then <p class="serious-error"> should be styled according to p.error. To do otherwise violates the fundamental purpose of @extend.

You shouldn't think about @extend in terms of the way it transforms the stylesheet. Its behavior in @media queries unfortunately forces some awareness of this, but in general you should think of it in terms of its motivating semantics.

I don't buy this one bit. "Why can't I @extend within using media queries" is a question that pops up on SO every now and then (in addition to the number of issues that are regularly opened here), so I would say the opposite is true: the fact that the styles aren't copied over is the unexpected behavior.

Users have two mutually unsatisfiable expectations. They expect @extend to adhere to its semantics within @media, and they expect it to generate a limited amount of CSS. We chose to throw an error when these two expectations came into conflict rather than silently failing to fulfill the second expectation. We did this because an error makes it visible that something undesirable is happening and gives the user the ability to explicitly decide to work around it, which is better than having their CSS silently balloon in size.

Until we get mixin interpolation or the ability to pass a mixin as an argument to another mixin, this isn't a reasonable solution in some instances. I get that you don't want to change how @extend behaves, so what about adding a @copy construct?

Mixin interpolation is coming.

@jakob-e
jakob-e commented Sep 9, 2013

Update.

I've added a cleaner and simpler version of "The Workaround" here:
Extending within the @media directive


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