Allow @extend across media queries #1050

Open
Snugug opened this Issue Dec 16, 2013 · 63 comments

Projects

None yet
@Snugug
Contributor
Snugug commented Dec 16, 2013

Edit: The current plan here is to allow @extend across media queries by duplicating the queries the current @extend is in and unifying them with any media queries the extendee is in. For example:

.a {w: x}
@media (min-width: 500px) {
  .b {@extend .a}
  .c {y: z}
}

would produce

.a {w: x}
@media (min-width: 500px) {
  .b {w: x}
}

@media (min-width: 500px) {
  .c {y: z}
}

and

@media screen {
  .a {w: x}
}

@media (min-width: 500px) {
  .b {@extend .a}
  .c {y: z}
}

would produce

@media screen {
  .a {w: x}
}
@media screen and (min-width: 500px) {
  .b {w: x}
}

@media (min-width: 500px) {
  .c {y: z}
}

Original issue follows:


As originally brought up in #456, one way to allow extending across media queries would be to have a flag for @extend to explicitly tell Sass that you're OK with creating a duplicate context in a similar fashion to how the !optional flag currently works.

The syntax as currently proposed would look/work something like the following:

%full {
  width: 100%;
  float: left;
  clear: both;
}

%half {
  width: 50%;
  float: left;
}

.sidebar-1 {
  @extend %full;
  background: blue;
  @media (min-width: 500px) {
    @extend %half !duplicate; // A new extend context will be created here for the all, min-width: 500px media context and extension will work as normal.
    background: red;
  }
}

.sidebar-2 {
  @extend %full;
  background: green;
  @media (min-width: 500px) {
    @extend %half !duplicate; // Because a context for this exact media query already exists, it will extend that one instead of creating a duplicate context
    background: orange;
  }
}

.content {
  @extend %half;
  background: purple;
}

would compile to

.sidebar-1, .sidebar-2 {
  width: 100%;
  float: left;
  clear: both;
}

.content {
  width: 50%;
  float: left;
}

.sidebar-1 {
  background: blue;
}

@media (min-width: 500px) {
  .sidebar-1, .sidebar-2 {
    width: 50%;
    float: left;
  }

  .sidebar-1 {
    background: red;
  }
}

.sidebar-2 {
  background: green;
}

@media (min-width: 500px) {
  .sidebar-2 {
    background: orange;
  }
}

.content {
  background: purple;
}

Of course the optimization around this would be difficult, needing to ensure that the matching only happens for identical @media contexts (but include ones in or chains) and a decision would need to be made as to if the selectors should be moved to the first or last item as the normal @extend pattern of "all" doesn't quite make sense here, but because it is an explicit call, a user will understand that they're changing how it works.

@robwierzbowski
Contributor

I like the idea of combining contexts/queries per extend, but not necessarily combining contexts/queries between different extends or with non-extended rulesets. My ideal would be:

SCSS

%half {
 width: 50%;
}

%full {
 width: 100%;
}

.one {
  @extend %half;
}

.two {
  @extend %full;
  @media (min-width: 30em) {
    @extend %half;
    color: blue;
  }
}

.three {
  @media (min-width: 30em) {
    @extend %full;
  }
}

CSS

.one {
  width: 50%;
}

@media (min-width: 30em) {
  .two {
    width: 50%;
  }
}

.two {
  width: 100%;
}

@media (min-width: 30em) {
  .three {
    width: 100%;
  }
}

.two {
  color: blue;
}

...keeping source order and general extend behavior as expected. For me, simplicity of behavior and sticking close to existing patterns trumps optimization. I'd prefer to combine all mqs with a post processor or optional Sass flag in a separate step.

I'm not sure what the status of this behavior (which has been proposed a couple times) is; if anyone knows of an issue tracking/rejecting it, please let me know. Really, anything that gets @extends and MQs working together is good for me.

@nex3
Contributor
nex3 commented Dec 30, 2013

@robwierzbowski If we add this flag, it will likely have the behavior you suggest. Changing the source order is something we're very keen to avoid.

@chriseppstein
Member

The issue here is that you have a pattern that is not properly abstracted. The pattern is that there is some elements which are full width for small screens that become half width for large screens. If you have expressly created this abstraction you will have an easier to understand stylesheet and Sass won't have to do backflips to optimize your unnamed abstractions.

@mixin column($width, $last: $width == 100%) {
  float: left;
  width: $width;
  @if $last {
    clear: both;
  }
}

@mixin for-large-screens {
  @media (min-width: 500px) {
    @content;
  }
}

%full {
  @include column(100%);
}

%half {
  @include column(50%);
}

@include for-large-screens {
  %half-lg {
    @include column(50%);
  }
}

%full-to-half {
  @extend %full;
  @extend %half-lg;
}

.sidebar-1 {
  @extend %full-to-half;
  background: blue;
  @include for-large-screens {
    background: red;
  }
}

.sidebar-2 {
  @extend %full-to-half;
  background: green;
  @include for-large-screens {
    background: orange;
  }
}

.content {
  @extend %half;
  background: purple;
}
.sidebar-1, .sidebar-2 {
  float: left;
  width: 100%;
  clear: both;
}

.content {
  float: left;
  width: 50%;
}

@media (min-width: 500px) {
  .sidebar-1, .sidebar-2 {
    float: left;
    width: 50%;
  }
}
.sidebar-1 {
  background: blue;
}
@media (min-width: 500px) {
  .sidebar-1 {
    background: red;
  }
}

.sidebar-2 {
  background: green;
}
@media (min-width: 500px) {
  .sidebar-2 {
    background: orange;
  }
}

.content {
  background: purple;
}

I remain unconvinced that Sass needs any magic for extend within runtime-based contexts. I find the above stylesheet more understandable than your original. Additionally, by reading it, it's very easy to deduce what the output will be.

@Snugug
Contributor
Snugug commented Jan 6, 2014

@chriseppstein The issue with what you've written is it actually goes against the best practices of responsive web design. When working with media queries, the best practice is to make changes as needed and not necessarily group them all together, especially true when it comes to grids as different layouts tend to ebb and flow much more than full-to-half. With the way you've described, every single different permutation of change between every single set of grids one may have across all permutations of media queries would need to be made available which is a maintenance nightmare and will easily become hard to decipher for developers. On the other hand, if each layout were to have their own extendable and could be called as such, that would make it infinitely easier to mix and match, and what was happening would be easier to grok.

Let's also not forget that this isn't just for layouts, it's for any number of items. Background image, clearfixes, box sizing, fonts stacks and definitions, all can benefit from being able to be extended from within a MQ and have a context created and needing to create extendable classes for all of their potential permutations seems like a maintenance headache as well.

@emagnier
emagnier commented Jan 6, 2014

+1, I'm completely agree with the latest comment of @Snugug.
I already had this need, and had to do some code gymnastics to get round this limitation. This gave things ​​less flexible, and more difficult to read and maintain.

@robwierzbowski
Contributor

My example was based off @Snugug's (which I think is valid). But disagreeing with the example is not a disagreement with the issue.

@chriseppstein Do you think we shouldn't be able to extend a(ny) value from within a media query? By which I mean apply <rules> to <selector>, where <selector> is .class or @media (foo) { .class }.

In my experience this is one of the most often requested features in Sass, both verbally among my peers and from what I see in issue queues and comment threads on the internet.

@nex3
Contributor
nex3 commented Jan 7, 2014

I want to continue considering this. Even if it's the case that there's a better factoring available for every stylesheet that wants to extend out of a media query, it's clear that users aren't able to see that refactoring easily. @robwierzbowski is right that this is highly-requested functionality. People run into the issue of extending out of media queries frequently, and I'd like a solution that we can at least explain in the error message.

@nex3 nex3 reopened this Jan 7, 2014
@glebm glebm referenced this issue in twbs/bootstrap-sass Jan 10, 2014
Closed

mixin for .sr-only? #498

@chriseppstein
Member

The definition of A { @extend B; } is to style elements matching A as if it matched selector B.

The definition of @media (some runtime expression) { A { @extend B; } } is to style elements matching A as if matching B when the media matches some runtime expression.

I'm not trying to tell people that they are wrong for wanting this to work. I think they are right for wanting this to work. Hell, I want this to work. But the bottom line is that a precompiler simply cannot implement this semantic without yielding output that we've deemed as too magical and bloat prone.

I'm love that you guys want this feature. You're preaching to the choir. You need to take this argument up with the CSS working group. Until then, I feel the @at-root directive provides enough of an escape hatch for people who know what they are doing to accomplish their needs.

I don't see any point in continuing to beat this dead horse.

@nex3
Contributor
nex3 commented Feb 24, 2014

I think it's reasonable to allow users to opt in to the extra bloat. As I mentioned above, it's clear that users aren't able to easily figure out how to use @at-root to work around this.

@chriseppstein
Member

@nex3 I'm not convinced. We've only just introduced @at-root and there is a learning/education period that is required to decide that. Furthermore, we have a way to produce bloat: mixins. I am against making @extend sometimes a selector operation and sometimes a rule copy operation. I'd rather see users write mixins that conditionally extend or include. This is not a feature Sass needs to add.

Additionally, I feel this will only increase the confusion about how people think that placeholder selectors can only be used like simple mixin declarations (having no arguments) rather than the powerful selector concept that they are.

@nex3
Contributor
nex3 commented Feb 24, 2014

We've only just introduced @at-root and there is a learning/education period that is required to decide that.

Can you summarize tersely how to take any cross-media @extend and make it work using @at-root? I don't fully understand the process myself.

Furthermore, we have a way to produce bloat: mixins. I am against making @extend sometimes a selector operation and sometimes a rule copy operation.

I would rather have users think of @extend as a semantic operation than in terms of its effect on the physical stylesheet. Making users switch between @extend and @include based on whether they're within a @media block makes the semantic abstraction more leaky, while having Sass make @extend itself work however necessary makes it less leaky.

Additionally, I feel this will only increase the confusion about how people think that placeholder selectors can only be used like simple mixin declarations (having no arguments) rather than the powerful selector concept that they are.

I'd rather focus our education efforts here than on complex work-arounds for @extend in @media.

@chriseppstein
Member

Can you summarize tersely how to take any cross-media @extend and make it work using @at-root? I don't fully understand the process myself.

@at-root (without: media) { & { extend .something; }} removes the runtime context so that the extend operation can be performed as if it were not within a media context. This is useful in places where the base definition is a constant across all media definitions or is appropriately overridden via the cascade even when applied across media types.

I would rather have users think of @extend as a semantic operation than in terms of its effect on the physical stylesheet.

I want this too. Opting-in using !duplicate or some other syntax makes the user think about it. Not requiring an opt-in causes huge surprise when the stylesheet bloats like using mixins. I think we have reached the boundary where @extend can be implemented in a preprocessor without being a leaky abstraction.

I'd rather focus our education efforts here than on complex work-arounds for @extend in @media.

There is going to be education required no matter how we tackle this problem.

@nex3
Contributor
nex3 commented Feb 25, 2014

@at-root (without: media) { & { extend .something; }} removes the runtime context so that the extend operation can be performed as if it were not within a media context. This is useful in places where the base definition is a constant across all media definitions or is appropriately overridden via the cascade even when applied across media types.

This doesn't scope the definition to the media query, though, which I think is what users are trying to express. Ensuring that the properties cascade so that a top-level extension works out is complicated and contingent on the specifics of the user's CSS.

I want this too. Opting-in using !duplicate or some other syntax makes the user think about it. Not requiring an opt-in causes huge surprise when the stylesheet bloats like using mixins. I think we have reached the boundary where @extend can be implemented in a preprocessor without being a leaky abstraction.

I agree that the abstraction is still leaky, but !duplicate (or whatever) makes it less leaky, and there's value in that. @extend is how users expect to be able to express this -- we have ample evidence of that from the volume of requests we get for it to work.

@robwierzbowski
Contributor

I really appreciate the points on both sides here, and I think they're
making the argument for extend with a flag very strong. It's a syntax that
users expect, and the code result proposed is no larger than any equivalent
code produced by at-root.

I agree with Nathan that users (like myself) specifically want to extend
the styles but scope them to the query. I can imagine a couple ways of
creating mixins with at root that would accomplish this, but none so direct
or understandable as processing at the Sass level with extend.

On Monday, February 24, 2014, Nathan Weizenbaum notifications@github.com
wrote:

@at-root (without: media) { & { extend .something; }} removes the runtime
context so that the extend operation can be performed as if it were not
within a media context. This is useful in places where the base definition
is a constant across all media definitions or is appropriately overridden
via the cascade even when applied across media types.

This doesn't scope the definition to the media query, though, which I
think is what users are trying to express. Ensuring that the properties
cascade so that a top-level extension works out is complicated and
contingent on the specifics of the user's CSS.

I want this too. Opting-in using !duplicate or some other syntax makes
the user think about it. Not requiring an opt-in causes huge surprise when
the stylesheet bloats like using mixins. I think we have reached the
boundary where @extend can be implemented in a preprocessor without being
a leaky abstraction.

I agree that the abstraction is still leaky, but !duplicate (or whatever)
makes it less leaky, and there's value in that. @extend is how users
expect to be able to express this -- we have ample evidence of that from
the volume of requests we get for it to work.

Reply to this email directly or view it on GitHubhttps://github.com/nex3/sass/issues/1050#issuecomment-35961979
.

Rob Wierzbowski
@robwierzbowski http://twitter.com/#!/robwierzbowski
http://github.com/robwierzbowski
http://robwierzbowski.com

@chriseppstein
Member

You both seem to be thinking that I'm disputing the use case. I'm not. I simply think that it's not a direction sass should go. I think the implementation is very tricky and the output is going to surprise users. even the ones who add the !duplicate flag because Sass says "Hey you gotta add the !duplicate flag to do this and it's going to copy all this stuff for ya".

Furthermore, I think Sass's set of existing abstractions enables users to accomplish this in "user space".

These are all the criteria that Nathan usually uses to justify not doing something and I'm usually the one arguing for Sass making things easier. I think he just likes to disagree with me. >_<

@robwierzbowski
Contributor

My thought is that there isn't a way with current abstractions to do what I and other users are asking. Is there a way to create the behavior in #1050 (comment) with four breakpoints and twelve collumns? I don't think a matrix of mixin-utilizing extends for each viewport and widths combination used (12col-to-6col-to-3col, 12col-to-12col, 8col-to-4col-pushed-2col-to-6col, etc.) is a realistic solution.

@chriseppstein
Member

@robwierzbowski Yes. As long as you can name each break point. Mixins, at-root, and extend are powerful enough to express the exact behavior that is desired. I guess I need to write a Sass library that demonstrates how.

@nex3
Contributor
nex3 commented Feb 26, 2014

Furthermore, I think Sass's set of existing abstractions enables users to accomplish this in "user space".

This is the crux of the issue for me. The user-space solution you're suggesting is complex and requires a large-scale restructuring of the surrounding Sass to make it work. Users are looking for a drop-in solution and you're proposing a full refactoring of their stylesheets. We have the power to provide precisely the semantics they're asking for; we can't do it for free, but I think conditionally adding bloat is a better compromise than trying to teach everyone a refactoring technique that it seems like no one but you understands.

@chriseppstein
Member

We have the power to provide precisely the semantics they're asking for

Well, the semantics I'm seeing asked for don't make sense to me. Sometimes it acts like extend and sometimes it acts like include. I very much dislike this. The most important aspect of @extend is that is preserves the function of the cascade. The proposal on the table here is to copy things to the location of the @extend statement -- this is going to change the cascade and makes the implementation complex because the extend step now has to do a lot more than selector rewriting.

Instead, I think a solution would need to add @media directives all over the document -- wherever an extended selector is found; just like we do with selectors now.

An example:

.a { prop1: value1; }
.b { prop2: value2; }
@media (...phone...) { .c { @extend .a; prop3: value3; } }
@media (...tablet...) { .d { @extend .b; prop4: value4; } }
.e .a { prop5: value5; }

would compile to:

.a { prop1: value1; }
@media (...phone...) { .c { prop1: value1; } }
.b { prop2: value2; }
@media (...tablet...) { .d { prop2: value2; } }
@media (...phone...) { .c { prop3: value3; } }
@media (...tablet...) { .d { prop4: value4; } }
.e .a { prop5: value5; }
@media (...phone...) { .e .c { prop5: value5; } }

The implementation of this is a lot less complex as well.

@cimmanon

Sometimes it acts like extend and sometimes it acts like include.

Newcomers to Sass don't understand the difference or why it matters.

The most important aspect of @extend is that is preserves the function of the cascade.

Again, most users don't understand this. All they understand is that it consolidates selectors, which means more compact CSS to them (though this isn't always the case). Users are disappointed that code like this doesn't work:

%clearfix {
    /* clearfix stuff */
}

.one {
    color: blue;
    @extend %clearfix;
}

.two {
    color: green;
    @media (min-width: 50em) {
        @extend %clearfix;
    }
}

.three {
    @extend %clearfix;
}

.four {
    color: orange;
    @media (min-width: 40em) {
        @extend %clearfix;
    }
}

The normal expectation is that it would generate something like this:

.one, .three {
    /* clearfix stuff */
}

.one {
    color: blue;
}

@media (min-width: 50em) {
    .two {
        color: green;
        /* clearfix stuff */
    }
}

@media (min-width: 40em) {
    .four {
        color: orange;
        /* clearfix stuff */
    }
}

To propose sprinkling extra media queries everywhere is the exact opposite of what users expect when they use extend (smaller CSS). Easier to write doesn't make for a good experience. Would adding a LESS-style include (where classes are also mixins with no arguments) really be that bad? I don't think anyone cares what it's called (extend vs include vs copy-it-here-because-i-said-so), they just want the behavior.

@robwierzbowski
Contributor

Sass isn't only for newcomers. Preserving source order at the expense of a
little more markup is a positive trade IMO.

Rob Wierzbowski
@robwierzbowski http://twitter.com/#!/robwierzbowski
http://github.com/robwierzbowski
http://robwierzbowski.com

On Wed, Feb 26, 2014 at 9:57 AM, cimmanon notifications@github.com wrote:

Sometimes it acts like extend and sometimes it acts like include.

Newcomers to Sass don't understand the difference or why it matters.

The most important aspect of @extend https://github.com/extend is that
is preserves the function of the cascade.

Again, most users don't understand this. All they understand is that it
consolidates selectors, which means more compact CSS to them (though this
isn't always the case). Users are disappointed that code like this doesn't
work:

%clearfix {
/* clearfix stuff */}
.one {
color: blue;
@extend %clearfix;}
.two {
color: green;
@media (min-width: 50em) {
@extend %clearfix;
}}
.three {
@extend %clearfix;}
.four {
color: orange;
@media (min-width: 40em) {
@extend %clearfix;
}}

The normal expectation is that it would generate something like this:

.one, .three {
/* clearfix stuff /}
.one {
color: blue;}
@media (min-width: 50em) {
.two {
color: green;
/
clearfix stuff /
}}
@media (min-width: 40em) {
.four {
color: orange;
/
clearfix stuff */
}}

To propose sprinkling extra media queries everywhere is the exact opposite
of what users expect when they use media queries (smaller CSS). Easier to
write doesn't make for a good experience. Would adding a LESS-style include
(where classes are also mixins with no arguments) really be that bad? I
don't think anyone cares what it's called (extend vs include vs
copy-it-here-because-i-said-so), they just want the behavior.

Reply to this email directly or view it on GitHubhttps://github.com/nex3/sass/issues/1050#issuecomment-36133080
.

@lolmaus
lolmaus commented Feb 26, 2014

Sometimes it acts like extend and sometimes it acts like include.

Does it make sense to add a separate, media query-friendly include directive?

@chriseppstein
Member

Newcomers to Sass don't understand the difference or why it matters.
All they understand is that it consolidates selectors

@cimmanon, Whether or not they understand that there is a fundamental theory behind @extend is irrelevant. There is, and that theory is what makes it work consistently in practice for all of our users.

To propose sprinkling extra media queries everywhere is the exact opposite of what users expect

I get it and it's why we're talking about it. But I'm ok with things not matching expectations as long as there is a clear explanation that will help them understand. It is easy to construct an example where the output that was originally suggested would differ from the behavior that is implied by the source code.

Does it make sense to add a separate, media query-friendly include directive?

Not to me. I don't see a new fundamental abstraction here. Ultimately, if we ever make an optimizer, it could clean up this output and coalesce media queries according to heuristics, optimization levels, etc.

@robwierzbowski
Contributor

If I understand it correctly, I'm all for Chris's last suggested
implementation. Sounds like exactly what I'd expect, and would be crazy
useful.

Rob Wierzbowski
@robwierzbowski http://twitter.com/#!/robwierzbowski
http://github.com/robwierzbowski
http://robwierzbowski.com

On Wed, Feb 26, 2014 at 11:45 AM, Chris Eppstein
notifications@github.comwrote:

Newcomers to Sass don't understand the difference or why it matters.
All they understand is that it consolidates selectors

@cimmanon https://github.com/cimmanon, Whether or not they understand
that there is a fundamental theory behind @extend is irrelevant. There
is, and that theory is what makes it work consistently in practice for
all of our users.

To propose sprinkling extra media queries everywhere is the exact opposite
of what users expect

I get it and it's why we're talking about it. But I'm ok with things not
matching expectations as long as there is a clear explanation that will
help them understand. It is easy to construct an example where the output
that was originally suggested would differ from the behavior that is
implied by the source code.

Does it make sense to add a separate, media query-friendly include
directive?

Not to me. I don't see a new fundamental abstraction here. Ultimately, if
we ever make an optimizer, it could clean up this output and coalesce media
queries according to heuristics, optimization levels, etc.

Reply to this email directly or view it on GitHubhttps://github.com/nex3/sass/issues/1050#issuecomment-36146497
.

@chriseppstein
Member

To be clear, I'm not in favor of adding a flag here. If we're going to allow @extend within directives having a runtime dependency then we should just allow it. Furthermore, the strategy I outlined above works fine for @supports, @media, @page and even unknown directives.

@nex3
Contributor
nex3 commented Feb 27, 2014

Well, the semantics I'm seeing asked for don't make sense to me. Sometimes it acts like extend and sometimes it acts like include. I very much dislike this.

By "semantics", I was referring to the styling semantics: the relationship between the Sass stylesheet and how the page is styled, not the relationship between the Sass stylesheet and the generated CSS. In terms of styling semantics, the proposed flag brings @extend closer to the stated goal of "this element should be styled as though it also matches this selector".

Instead, I think a solution would need to add @media directives all over the document -- wherever an extended selector is found; just like we do with selectors now.

Sorry, I should have been clearer: this is what I'm arguing for. I didn't read @Snugug's example closely enough to figure out that it wasn't identical to this.

To be clear, I'm not in favor of adding a flag here. If we're going to allow @extend within directives having a runtime dependency then we should just allow it. Furthermore, the strategy I outlined above works fine for @supports, @media, @page and even unknown directives.

I think a flag is important to avoid users having massive unexpected bloat, although I'm open to being convinced otherwise.

@chriseppstein
Member

Sorry, I should have been clearer: this is what I'm arguing for. I didn't read @Snugug's example closely enough to figure out that it wasn't identical to this.

👊

I think a flag is important to avoid users having massive unexpected bloat

This is Sass's curse for many of it's features. I support flags that change behavior or imply making something succeed that would fail/warn otherwise. This just implies that the user understands what they are doing. They need to understand this once, but then we force them to type this flag for all eternity. I'd rather them just learn this by reading the output and asking "why?".

@robwierzbowski
Contributor

They need to understand this once, but then we force them to type this flag for all eternity. I'd rather them just learn this by reading the output and asking "why?".

👍 🚀 💇

@strann
strann commented Mar 19, 2014

I was ready to write an example usecase for how we'd love to be able to use @extend within a media query, but @cimmanon beat me to it. The clearfix example further up the thread is exactly what I was going to show. Until we can do exactly that, Sass 3.3 is crippled for making responsive sites while abstracting out code into placeholders (which is essential in my opinion).

After much exploring over the last couple years, by far the best way to handle responsive Sass is to write a module for mobile first, then sprinkle nested media queries in that module to keep everything in one, easily understood place. We simply can't do that reliably now because we can't use our placeholders the way we want to.

I'd love to see either solution. A flag on @extend to allow a new rule context within the media query or more media queries. Though it could be argued that the latter approach would bloat your output even more than starting a new rule context in the given media query. Really hope you guys come to an agreement on how to proceed.

@lolmaus
lolmaus commented Mar 19, 2014

I would like to add that for the responsive use case i only want to extend placeholder selectors (that start with %) from inside media queries. It has never been a requirement to extend normal selectors from media queries.

@chriseppstein
Member

@lolmaus There's no reason to special case placeholder selectors. Any solution that works for placeholders will work for standard css selectors.

@lolmaus
lolmaus commented Mar 19, 2014

Please forgive my dumbness, please explain one more time why it is unacceptable to maintain a separate set of extendables for every unique media query (including the-outside-of-media-queries as one of them).

Is it technically too difficult or is it just the matter that users will extend from media queries without understanding the mechanics?

@chriseppstein
Member

@lolmaus The premise of your question seems to be that we've decided to not add this feature. but if you read up, you'll see that is no longer the case.

@lolmaus
lolmaus commented Mar 19, 2014

@chriseppstein My impression was that you and @nex3 decided that the feature is necessary but neither agreed on resulting behavior nor confirmed that it's technically feasible.

@chriseppstein
Member

@lolmaus AFAICT, we disagree only about whether there should be an opt-in flag.

@jslegers

Herebelow is my solution for dealing with placeholders and media queries. It's not a perfect solution by any standard, but it's the best I've been able to come up with considering what Sass 3.3 allows me to do...

In spite of its limitations, it's a far better solution than anything I could come up with using Sass 3.2. I really love the various additions to Sass that were made in version 3.3. There's but a few features still missing for me to truly embrace the language...

Configuration

// Configuration :
// -----------------------------------------

$default-media-query : 'default';
$screensizes : (
  'default' : 0 infinity,
  'mobile' : 0 767px,
  'phone' : 0 480px,
  'tablet' : 481px 767px,
  'desktop' : 768px 979px,
  'widescreen' : 1200px infinity
);

String functions

// Adding some missing string functions :
// -----------------------------------------

@function str-replace($string, $old, $new : '') {
    @if type-of($string) != string {
        $string : '' + $string;
    } 
    $index: str-index($string, $old);
    @while $index and $index > 0 {
        $temp : $string;
        $string : $new;
        @if $index > 1 {
            $string : str-slice($temp, 1, $index - 1) + $string;
        }
        @if $index < str-length($string) {
            $string : $string + str-slice($temp, $index + 1);
        }
        $index: str-index($string, $old);
    }
    @return $string;
}

@function str-clean($string, $dreplacements: (
        '%' : '-perc-',
        '(' : '-l-b-',
        ')' : '-r-r-',
        '/' : '-ps-',
        '"' : '-dq-',
        "'" : '-sq-',
        '.' : '-dot-',
        ' ' : '-nbsp-'
    )) {
    @each $old, $new in $dreplacements {
        $string : str-replace($string, $old, $new);
    }
    @return $string;
}

The magic

// The 'magic' :
// -----------------------------------------

$dynamic-placeholders: ();
$current-media-query : $default-media-query;

@mixin generate-root-placeholder($placeholder, $styles) {
    @at-root %#{$placeholder} {
        @each $rule, $style in $styles {
            #{$rule}: $style;
        }
    }
    $dynamic-placeholders: append($dynamic-placeholders, $placeholder) !global;
}

@mixin extend-root-placeholder($placeholder) {
    @extend %#{$placeholder} !optional;
}

@mixin rules($styles) {
    @each $rule, $style in $styles {
        $value : str-clean($style);
        $placeholder : #{$current-media-query}-cascade-#{$rule}-#{$value};
        @if not index($dynamic-placeholders, $placeholder) {
            @include generate-root-placeholder($placeholder, ($rule : $style));
        }
        @include extend-root-placeholder($placeholder);
    }
}

@mixin ruleset($styles) {
    $placeholder : #{$current-media-query}-cascade;
    @each $rule, $style in $styles {
        $value : str-clean($style);
        $placeholder : #{$placeholder}-#{$rule}-#{$value};
    }
    @if not index($dynamic-placeholders, $placeholder) {
        @include generate-root-placeholder($placeholder, $styles);
    }
    @include extend-root-placeholder($placeholder);
}

@mixin respond-to($media) {
    $range : map-get($screensizes, $media);
    @if $range == null {
        @warn 'Unknown screensize "#{$media}" for @mixin respond-to';
    } @else {
        $current-media-query : $media !global;
        $min-width : nth($range, 1);
        $max-width : nth($range, 2);
        @if $min-width > 0 {
            @if $max-width != infinity {
                @media only screen and (min-width: $min-width) and (max-width: $max-width) { @content; }
            } @else {
                @media only screen and (min-width: $min-width) { @content; }
            }
        } @else {
            @if $max-width != infinity {
                @media only screen and (max-width: $max-width) { @content; }
            } @else {
                @content;
            }
        }
        $current-media-query : $default-media-query !global;
    }
}

Examples

// Examples :
// -----------------

// Basic examples without a media query
.s1 {
  @include rules((
    float : right,
    width : 30%,
    background : #fff,
  ));
}

.s2 {
  @include rules((
    float : right,
    width : 100%,
    background : #000,
  ));
}

.s3 {
  @include rules((
    float : left,
    width : 30%,
    background : #fff,
  ));
}

.s4 {
  @include rules((
    float : left,
    width : 40%,
    background : 'url(image/cool-background.jpg)',
  ));
}

// Here are some examples with media queries
// Note that "respond-to(default)" actually has the
// same impact as not using "respond-to" at all.
// It is actually purely optional.
// I added this feature for the sake of
// readability only.
.s5 {
  @include respond-to(default) {
    @include ruleset((
      float : right,
      width: 100%
    ));
    @include rules((
      background : 'url(image/cool-background.jpg)'
    ));
  }
  @include respond-to(mobile) {
    @include rules((
      width : 40%
    ));
  }
}

.s6 {
    @include respond-to(default) {
        @include rules((
            width: 100%
        ));
    }
    @include respond-to(mobile) {
        @include ruleset((
            float: right,
            width: 300px
        ));
    }
    @include respond-to(desktop) {
        @include rules((
            width: 125px
        ));
    }
    @include respond-to(widescreen) {
        @include rules((
            float: none
        ));
    }
}

.s7 {
    @include respond-to(default) {
        @include rules((
            float: left,
            width: 100%
        ));
    }
    @include respond-to(desktop) {
        @include rules((
            width: 125px
        ));
    }
    @include respond-to(phone) {
        @include ruleset((
            width: 400px
        ));
    }
    @include respond-to(widescreen) {
        @include rules((
            float: none
        ));
    }
}

The output

.s1, .s2 {
  float: right;
}
.s1, .s3 {
  width: 30%;
}
.s1, .s3 {
  background: white;
}

.s2, .s6, .s7 {
  width: 100%;
}
.s2 {
  background: black;
}

.s3, .s4, .s7 {
  float: left;
}

.s4 {
  width: 40%;
}
.s4, .s5 {
  background: "url(image/cool-background.jpg)";
}

.s5 {
  float: right;
  width: 100%;
}
@media only screen and (max-width: 767px) {
  .s5 {
    width: 40%;
  }
}

@media only screen and (max-width: 767px) {
  .s6 {
    float: right;
    width: 300px;
  }
}
@media only screen and (min-width: 768px) and (max-width: 979px) {
  .s6, .s7 {
    width: 125px;
  }
}
@media only screen and (min-width: 1200px) {
  .s6, .s7 {
    float: none;
  }
}

@media only screen and (max-width: 480px) {
  .s7 {
    width: 400px;
  }
}
@unyo
unyo commented Sep 9, 2014

Is it possible to turn something like this...

@mixin common-image-area() {
    background-size: cover;
    background-position: center center;
    padding: 25px;
    color: white;
    text-align: center;
}
%common-image-area {
    @include common-image-area();
}
.my-semantic-class {
    @extend %common-image-area;
}
.other-semantic-class {
    @extend %common-image-area;
}
@media (min-width: 768px) {
    %common-image-area-sm {
        @include common-image-area();
    }
    .media-semantic-class {
        @extend %common-image-area-sm;
    }
    .media-semantic-class-2 {
        @extend %common-image-area-sm;
    }
}

...into this?

~common-image-area {
    background-size: cover;
    background-position: center center;
    padding: 25px;
    color: white;
    text-align: center;
}
.my-semantic-class {
    @extend ~common-image-area; // this works like %, copying ~common-image-areas's contents into .my-semantic-class
}
.other-semantic-class {
    @extend ~common-image-area; // this works like %, extending .my-semantic-class with .other-semantic-class
}
@media (min-width: 768px) {
    .media-semantic-class {
        @extend ~common-image-area; // this works like a cross-media %, copying ~common-image-area's contents into .media-semantic-class;
    }
    .media-semantic-class-2 {
        @extend ~common-image-area; // this works like a cross-media %; extending .media-semantic-class with .media-semantic-class-2
    }
}

Right now in order to keep things DRY, I need to A) create a @mixin where the bulk of the styling is, B) create a custom named %placeholder for every @media query, and C) @extend that custom named placeholder for every media query.

If I have 4 @media queries, that means I need to make %common-image-area, %common-image-area-2, %common-image-area-3, and %common-image-area-4 to work around the error message.

Maybe I'm missing something? I looked at the @at-root example but I don't think that accomplishes the above effect. Are there any examples on how to do this the SASS way?

@nex3
Contributor
nex3 commented Sep 12, 2014

@unyo If the semantics you're looking for are just "use this set of styling for a bunch of classes", just use a mixin.

@chriseppstein
Member

@nex3 The issue with mixins is the bloated output. people want to condence the output using comma selectors. I've been thinking that maybe we can simply add support to @extend to extend mixins.

@nex3
Contributor
nex3 commented Sep 12, 2014

The sort of bloat that mixins produce is the sort that gzip is very good at handling. I'd like to see data indicating that the post-gzip size of mixin-based stylesheets is substantially worse than extend-based equivalents before we compromise the semantics of @extend.

It also seems like the sort of thing that a post-perform optimization step could plausibly handle, depending on how smart it can be about combining rules.

@chriseppstein
Member

@nex3 a few points:

  1. only 58% of web traffic is gzip compressed. better output for the other 42% seems prudent.
  2. Output that approximates hand-optimized CSS is a part of the craftsmanship that I want to support for our authors.
  3. I don't think there's a safe optimization that will do a good job here. But if there is, that is clearly and interesting direction and I'd be interested in knowing how you think that would work.
@nex3
Contributor
nex3 commented Sep 19, 2014

only 58% of web traffic is gzip compressed. better output for the other 42% seems prudent.

I think it's a pretty safe assumption that most people who care about their CSS size are in that 58%. At least, safe enough not to warrant adding additional features.

Output that approximates hand-optimized CSS is a part of the craftsmanship that I want to support for our authors.

I like nice output, but I like it less than nice input semantics. I want to encourage users to think of Sass as a language that affects the styling of their web pages, not a language that generates text. Adding a feature that purely affects text generation goes against that.

I don't think there's a safe optimization that will do a good job here. But if there is, that is clearly and interesting direction and I'd be interested in knowing how you think that would work.

There's not a safe optimization in the fully general case, but there's a lot of local logic we could do to determine which rules can be reordered safely based on whether or not their properties and selectors overlap. It's an empirical question how successful this logic will be, but I suspect it can make a reasonable amount of difference in some cases.

@jakob-e
jakob-e commented Oct 14, 2014

A simple solution (without the bloat)

By using a global flag to keep track of media context and creating unique extend
names (context prefix) it is fairly simple to work with extends across media queries....
but it took me some time to figure it out ;-)

Try it out: http://sassmeister.com/gist/f90d77532f95078a29fc
tested on v.3.3.14

_media.scss

// Variable to keep the current media context
$__current_breakpoint_key: '';

// Simple media mixin 
@mixin media($breakpointkeys...){
    @each $key , $value in $breakpointkeys {
        $__current_breakpoint_key: $key !global; // set flag on enter
        @media #{map-get($breakpoints, $key)}{ @content; }    
        $__current_breakpoint_key: '' !global;  // reset on exit
    }
}

// Function to get the current media context 
@function media-context(){
    @return $__current_breakpoint_key; 
}

// Mixin to clone placeholders to each media directive
// - cuts down on the amount of generated media queries :)
@mixin extends(){
    @content;
    @each $key, $value in $breakpoints {
        @include media($key){ @content; } 
    } 
}

// Mixin to create media placeholders 
@mixin new-extend($extend-name){
    %#{ media-context() + $extend-name }{ @content; }
}

// Mixin to extend existing media placeholders
@mixin extend($extend-name){
    & {
        @extend %#{ media-context() + $extend-name }
    }
}

styles.scss

// Import the stuff above
@import '_media.scss';


// Define your breakpoints
$breakpoints:(
      all     :'all'
    , mobile  :'(max-width: 480px)'
    , palm    :'(min-width: 481px) and (max-width: 680px)'
    , tablet  :'(min-width: 681px) and (max-width: 960px)'
    , desktop :'(min-width: 961px) and (max-width: 1200px)'
    , large   :'(min-width: 1201px)' 
); 

// Create the media extends (including the normal %)
// Try to add all new-extends in one place to 
// minimize the number of CSS media queries 
@include extends(){
  @include new-extend(foo){ content:' foo '; context: media-context(); }
  @include new-extend(bar){ content:' bar '; context: media-context(); }  
  @include new-extend(doh){ content:' doh extending foo'; @include extend(foo); }  
}

// Test A - root
.testA {
  // Outside directives all new-extends can be extend like normal 
  // ... or by using @include extend(name)  
  @extend %foo;           
  @include extend(bar); 
}

// Test B - nested 
.testB {
  @include media(mobile){   
    @include extend(foo);  // Extending inside a media directive
  }
  @include media(palm, tablet){
    @include extend(bar); // Extending inside multiple media directives
  }
  @include media(desktop){
    @include extend(doh);  // Extending an extending extend
  }
}

// Test C - wrapped
@include media(mobile, tablet){
  .testC1 { @include extend(foo);  }  
  .testC2 { @include extend(bar);  }  
  .testC3 { @include extend(doh);  }  
}


// Test - Media Context
@function font-size-by-context($context){
    @return map-get((
      all     : 1em
    , mobile  : 2em
    , palm    : 3em
    , tablet  : 4em
    , desktop : 5em
    , large   : 6em
    ), $context);
}

@function font-color-by-context(){
    $context:media-context();
    @return map-get((
      all     : black
    , mobile  : red
    , palm    : lighten(green, 10%)
    , tablet  : green
    , desktop : gold
    , large   : darken(gold, 10%)
    ), $context);
}

@include media(all, mobile, palm, tablet, desktop, large){
    body:before { 
        content : 'Current context is: #{media-context()}';
        font-size: font-size-by-context(media-context());
        color: font-color-by-context();
    }
}

styles.css

.testA { content: ' foo '; context: ""; }
.testA { content: ' bar '; context: ""; }

@media (max-width: 480px) {
  .testC3, .testB, .testC1 { content: ' foo ';  context: mobile; }
  .testC2 { content: ' bar '; context: mobile; }
  .testC3 { content: ' doh extending foo'; }
}

@media (min-width: 481px) and (max-width: 680px) {
  .testB { content: ' bar '; context: palm; }
}

@media (min-width: 681px) and (max-width: 960px) {
  .testC3, .testC1 { content: ' foo '; context: tablet; }
  .testB, .testC2 { content: ' bar '; context: tablet; }
  .testC3 { content: ' doh extending foo'; }
}

@media (min-width: 961px) and (max-width: 1200px) {
  .testB { content: ' foo '; context: desktop; }
  .testB { content: ' doh extending foo'; }
}


/** Media Context **/
@media all {
  body:before { content: "Current context is: all"; font-size: 1em; color: black; }
}
@media (max-width: 480px) {
  body:before { content: "Current context is: mobile"; font-size: 2em; color: red; }
}
@media (min-width: 481px) and (max-width: 680px) {
  body:before { content: "Current context is: palm"; font-size: 3em; color: #00b300; }
}
@media (min-width: 681px) and (max-width: 960px) {
  body:before { content: "Current context is: tablet"; font-size: 4em; color: green; }
}
@media (min-width: 961px) and (max-width: 1200px) {
  body:before { content: "Current context is: desktop"; font-size: 5em; color: gold; }
}
@media (min-width: 1201px) {
  body:before { content: "Current context is: large"; font-size: 6em; color: #ccac00; }
}

Jakob E

PS - If you are using breakpoint by team sass you can:


1) Replace    @media #{map-get($breakpoints,$key)}{ @content; }
   with this  @include respond-to($key, $no-query:false){ @content; };

2) Add your breakpoints
   $breakpoints: add-breakpoint(mobile , min-width 480px);
   $breakpoints: add-breakpoint(tablet , 481px 960px);
   $breakpoints: add-breakpoint(desktop, 961px);

3) And use media(mobile,...) instead of respond-to(mobile) 
   ...Hope you don't hate me for changing the name back ;-)

This was referenced Oct 15, 2014
@OliverJAsh

I just read this discussion in its entirety to reach a conclusion on the status. I gather the consensus is to go with the solution described by @chriseppstein in #1050 (comment). If that's the case, could we create a new issue describing the issue, so folk such as me can track the progress? The title of this issue is now irrelevant and thus misleading.

@nex3
Contributor
nex3 commented Dec 12, 2014

I don't want to create a new issue, but I'll update the original issue to describe the current thinking.

@nex3 nex3 changed the title from Add flag to allow @extend across media queries to Allow @extend across media queries Dec 12, 2014
@Ajedi32
Ajedi32 commented Apr 10, 2015

👍 For this. Personally, I like to think of extend as a bit like class inheritance in programing languages. If I say:

.a {
  color: blue;
}

@media (max-width: 200px) {
  .some-class {
    @extend .a;
  }
}

I expect that .some-class will inherit from .a (and thus have blue text) when the device is under 200 pixels wide. I don't care about the output CSS any more than I care about the binary output when I write in C++; it's an implementation detail. As long as the resulting code's behavior matches my expectations, I don't really care.

@davidkpiano

I think this should be allowed with the constraint that only rulesets defined outside of any media queries should be "extendable" within any media query context. I would still (hopefully) expect this to throw an error:

.outside { ... }

@media (max-width: 100px) {
  .foo { ... }
}

@media (max-width: 400px) {
  .bar { @extend .foo; } // should still throw an error
  .baz { @extend .outside; } // acceptable
}
@Ajedi32
Ajedi32 commented Apr 10, 2015

@davidkpiano That would be an acceptable compromise if it helps ease implementation. Eventually though, I'd expect even that to work by combining the media queries with and.

.foo {
  color: red;
  border-bottom: 2px solid black;
}

@media (print) {
  .foo {
    color: black;
  }
}

@media (min-width: 400px) {
  .bar {
    @extend .foo; /* This should have a black font on print media with width larger than 400px */
  }
}

(Sorry for the contrived example.)

@mgussekloo

This would be a useful feature.

@tjbenton tjbenton referenced this issue in eduardoboucas/include-media May 18, 2015
Closed

Added support for an each media functionality #31

@roll
roll commented Jun 19, 2015

👍

@luksak
luksak commented Aug 18, 2015

I totally agree with @nex3.

Having this feature is much more important than caring about bloat that only affects non-gzipped CSS.

I also think that hand-optimized CSS is important, but in that case you might not use this feature, right?

Post-optimization is definitely possible. The question is how good it will be while staying safe.

@nex3 nex3 added Planned and removed Under Consideration labels Aug 22, 2015
@chriscartlidge

👍

@mattidupre

Was looking for a workaround and stumbled on benschwarz/metaquery. TL;DR: use Javascript to apply classes to instead of media queries.

Otherwise I completely agree that this issue should be worked out. @extend is a huge chunk of SASS functionality, and at the point where developers really care that much about file size they will be smart enough to optimize a different way.

@ruanmer
ruanmer commented Feb 5, 2016

+1

@vandres
vandres commented May 27, 2016

+1

@t22james

noticing that this ticket has very teasingly been given a 'planned' label... do we have any update of timelining for release?

@chriseppstein
Member

@t22james we have a design, but it's not slated for any release at this time.

@matthewmorek

@chriseppstein Please, keep us updated on the progress, as there are a lot of us waiting for this feature to be added to Sass as soon as this is possible.

@sospedra

3 years later still not done...

@brandensilva
brandensilva commented Oct 4, 2016 edited

Many of us are still waiting on this feature to land. It seems like with all the discussion previously around this it'd have some more weight in its priority.

The fact that you can't @extend from within a media query feels awkward. Can you generate mixins and workarounds? Sure, but you just expect it to work no matter where you @extend from as long as whatever you are extending is available to the file you are using it in.

@nagyalex
nagyalex commented Jan 4, 2017

Another vote for this functionality.

There are hack workarounds, but they are troublesome with CSS frameworks like Bootstrap.

@jakdotspiral
jakdotspiral commented Jan 12, 2017 edited

Are we any closer with this? I presume it's a case of not merging the media queries that contain @extends with the ones that don't? I doubt it's an easy feat either way.

I could really do with extending some utility classes (to maintain consistency above all) where it's otherwise awkward to use them in the markup and with the @<breakpoint> suffix.

@liuliangsir

+1

@lucidstack lucidstack referenced this issue in unepwcmc/unep-wcmc.org Mar 21, 2017
Stacy Talbot Start to change the view from haml to HTML 1688127
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment