sass/sass

Extending a compound selector violates extend semantics

nex3 opened this issue Β· 95 comments

nex3 commented

Currently, when a compound selector is extended, Sass will only replace instances of the full extendee with the extender. For example:

.a {x: y}
.b {x: y}
.a.b {x: y}

.c {@extend .a.b}

produces

.a {x: y}
.b {x: y}
.a.b, .c {x: y}

when it should produce

.a, .c {x: y}
.b, .c {x: y}
.a.b, .c {x: y}

according to the semantics of @extend. We should fix this, especially as CSS moves closer to supporting @extend natively.

Tasks:

  • Deprecate existing behavior in stable.
  • Remove behavior from master.

@nex3 any ideas when this might be fixed?

To clarify:

.c { @extend .a.b } means "Style elements with class c, as if they had both of the classes .a and .b", It's semantically identical to .c { @extend .a; @extend .b; }

To be honest, I'm slightly confused.

This example seems to be perfectly right result for .c {@extend .a.b}:

.a {x: y}
.b {x: y}
.a.b, .c {x: y}

It's because we clearly asked to extend .a with specified by .b selector, not just .a or .b, or .a.b.

If we'd like to receive this:

.a, .c {x: y}
.b, .c {x: y}
.a.b, .c {x: y}

I'd more expect syntax like

.a {x: y}
.b {x: y}
.a.b {x: y}

.c {@extend .a, .b, .a.b}

or

.a {x: y}
.b {x: y}
.a.b {x: y}

.c {@extend .a.; @extend .b; @extend .a.b}
nex3 commented

@ArmorDarks When you write A {@extend B}, that means "all elements that match A should be styled as though they also matched B". In the example above, that translates to "all elements that match .c should be styled as though they also matched .a.b". If an element matches .a.b, that means it also matches both .a and .b individually, so .c {@extend .a.b} should mean the same thing as .c {@extend .a; @extend .b}.

If an element matches .a.b, that means it also matches both .a and .b individually, so .c {@extend .a.b} should mean the same thing as .c {@extend .a; @extend .b}.

I still miss the logic why .a.b. also matches .a and .b individually. Those are 3 different selectors, it just so happens that they consist from same elements, but nothing more.

It just goes against CSS logic, since when in CSS we write more complex selectors, we increasing its specificity on purpose, to ensure that only "those instances of class" with class='a b' will receive that style, not class='a' and class='b'.

I think Sass can follow pretty much close same logic β€” when you increase specificity of extend, you want to extend only specific selectors, not everything related to them, otherwise you would write more generic extend.

@ArmorDarks that's the behavior I would expect. @extend should extend the selector that is passed to it. Anything more would risk yielding unexpected results, not to mention further tarnish it's already eroded reputation.

-1 to this. If I want .c to extend .a and .b I write .c { @extend .a; @extend .b; }.

I'm receiving deprecation warnings linking to this issue for several bits of SCSS that look like this:

.icon-check:before { content: "\f13e"; }

.foo {
  // [...]
  &:after {
    // [...]
    @extend .icon-check:before;
  }
}

This SCSS should probably be rewritten, but that's not the point; I don't think it's semantically wrong.
Why is it triggering this warning?

nex3 commented

@jdurand :before is an unusual case; see this comment for more specific discussion about it in particular.

@nex3 thanks for linking this.
Though what I get from your explanation is that this legitimate use case is deprecated for a somewhat abstract reason. Anyway, I don't mind freezing my SASS version for this, I just don't really understand why this is being cut out.

nex3 commented

Think of it this way: we had unintended behavior that didn't match the semantics of the operation and made implementations slower and harder to write, but that happened to do the right thing in one edge case. We decided that on the whole that one edge case was not worth the burden of supporting the entire set of behavior.

OK, I guess this makes sense.
BTW, thank you for your great work! I don't think it's being said enoughπŸ₯‡

Cheers!

I'm receiving deprecation warnings linking to this issue for this code:

button {
  &.link {
    @extend a;
    &:hover {
      @extend a:hover;
    }
  }
}

Extending a:hover now issues the warning.

Is there a recommended way of refactoring this code to make it future-proof? I was looking on the web, but did not find anything.

nex3 commented

The second @extend in that example isn't really doing anything. When you write button.link {@extend a}, that's already going to add button.link:hover to any a:hover selectors in your document.

Before that change, it was possible to extend exactly :hover. It was useful, when element in unhovered state had to look like exactly as another hovered element.

But in this case it seems that extending a indeed enough, unless @gerdriesselmann meant something like this:

button {
  &.link {
    @extend b;
    &:hover {
      @extend a:hover;
    }
  }
}

Btw, worth noting, that using something like BEM methodology mostly eliminates that issue, since you will almost never have compound selectors.

Though, pseudo-selectors and some occasional JS-related state classes (like .is-active) which requires chaining even in BEM still will preserve some issues.

@nex3 @ArmorDarks Thank you! It is indeed an anchor tag, that gets extended here. So I'm glad I can just delete it.

So here's a real life example:

.control-label {
  &.required::after {
    content: '*';
    font-size: 150%;
    line-height: 0;
    @extend abbr[title];
  }
}

How can we achieve this?

Should we extend .control-label with abbr styles? I don't think this is desired and that this is how it works now.

nex3 commented

This is a reduction in functionality; there's no direct equivalent to that style. That's because it doesn't make sense to talk about the styles that apply to elements matching abbr[title] without also talking about the styles that apply to elements matching each of those selectors individually. If you're writing a selector like that, your styles are at odds with how CSS works, and that's not something we want to encourage in Sass.

Another way to think about what CSS defines as equivalent. For example, in the absense of other styles, this CSS:

abbr {color: blue}
abbr[title] {color: red}

should work exactly the same as this CSS:

abbr {color: red}
abbr:not([title]) {color: blue}

But under the old semantics, Sass breaks that equivalence. @extend abbr[title] makes text red in one but not the other. This is a kind of contrived example, but the broader point is that the author of the CSS you're extending has valid expectations that the styles they write for abbr[title] will cascade with the styles they write for abbr, and the old extend semantics violated those expectations.

None of the above, including what did the author of abbr[title] expected while writing that style makes our style invalid, it just does exactly what we need and proves that the decision to deprecate this functionality is wrong and is just a reduction in functionality. Btw, the style of abbr[title] is defined in twitter bootstrap and we just want to copy it to another element, which is pretty much a valid use case. SASS is just an instrument, it doesn't break anything but developers do and there are use cases when you have to.

This broke our code completely in gtk themming. We use

@mixin exports($name) {
    @if (not index($modules, $name)) {
        $modules: append($modules, $name) !global;

        @content;
    }
}


@include exports("button_extends") {
    %linked_button { 
        border-width: 1px;
        &:first-child { border-width: 0; }
    }
}
@mixin linked_button($bg) {
   .button.combo > :not(.vertical) > &:first-child { @extend %linked_button:first-child; }
}

This produces deprecated warnings. If I modify the code, it breaks the themming in gtk.

How this can be modified according to new rule? Shouldn't it bypass the export rule?

nex3 commented

@khurshid-alam I'd recommend writing the linked_button mixin as:

@mixin linked_button($bg) {
   .button.combo > :not(.vertical) > & { @extend %linked_button; }
}
.a {x: y}
.b {x: y}
.a.b {x: y}

.c {@extend .a.b}

This works perfectly fine to me and works right. If i wanted to extends all of them, i would use @extend .a;@extend .b;.

I use this feature a lot when porting css/scss to another project, because sometimes the combo-class are different (for instance, it can be .c and .d). Without this feature, the porting woud be painfull.

I use this a lot in WebFlow. WebFlow doesn't have inheritance, so i need to use combo-classes to simulate it (.item.child) and then in my project i use .parent > .item {@extends .item.child; } to make it work as intended.

Another example would be porting JS components. In an project i would use .item.active and them port to .dropdown.visible

Please don't deprecate this feature, unless there is an good reason for it.

One good solution for this would be:

.a {x: y}
.b {x: y}
.a.b {x: y}

.c {@extend .a, .b, .a.b}

or if it will really be removed, unless make an option to still use it like:

.c {
   @extend .a.b !compound;
}

That's because it doesn't make sense to talk about the styles that apply to elements matching abbr[title] without also talking about the styles that apply to elements matching each of those selectors individually

I'm reading this again and again, but it doesn't make sense to me. Sorry.

We were explicit here. We asked one part of element to match only part of another element, not its whole.

@nex3

I don't need { @extend %linked_button; } but rather { @extend %linked_button:first-child; }

@ArmorDarks

I understand your reason for general cases. But Does it valid for following example?

    %a { 
        border-width: 1px;
        &:b { border-width: 0; }
    }
}

y {@extend a:b; } /*means y {border-width: 0;}*/

What do you propose for this without writing duplicate codes all over?

It isn't valid even for classes, .a.b does not mean .a + .b, looks like a misunderstanding. I agree .a.b will inherit styles from .a and from .b, but is it necessary that those are defined? No, so the final decision should be taken by the developer, not by sass. This is a very particular case and a global rule doesn't work well here for obvious reasons.

Well, I guess this feature has been removed at first place because of maintaining complexity and uncertainty in logic, and only in second place because of some assumed discrepancy with CSS semantics (which is questionable as described here #1599 (comment)).

If this statement is true, nothing can be done about it. Otherwise it would be possible to come up with some other syntax, which will imply extending exclusively of certain complex CSS selector part.

Something like

.test { @extend .a -> disabled }
// horrible example, but nevermind

As an alternative, if something like CSS fragments will be implemented and it will become possible to treat every selector as an object and mixin, it would be possible to achieve same effect without extending, but with inclusion:

.a { 
  border-width: 1px;

  &:after {
    border-width: 0;
  }
}

.result { @include .a:after; } // => { border-width: 0; }

There is no ambiguity here, since here we asking to include certain part and not whole selector.

I still don't really get why this feature was deprecated.

From what I can see, it makes perfect sense for it to work how it did.

Say I have this CSS:

button { /*code*/ }

button.a { /*code*/ }

button.b { /*code*/ }

button.a.b { /*code*/ }

If I wanted to extend the styling of button, I'd do @extend button. If I wanted to extend button.a, I'd do @extend button.a. If I wanted to extend button.b, I'd do @extend button.b.

But for some reason if I wanted to extend button.a.b, I can't do @extend button.a.b? I want button to have separate functionality for .a, .b and .a.b, which seems like a perfectly logical thing to want to happen.

This really doesn't make sense, especially if you're trying to style something exactly like another element (trying to style a <input type="button"/>, for example). If @extend <selector> takes the contents of <selector> {} and puts it in your code, it just doesn't make sense to exclude certain selectors arbitrarily.

Reading through this thread, it seems like this change has broken a number of real-world projects that used the functionality in the way that seemed most logical. Heck, that's how I stumbled across this issue.

The only fix seems to be to just copy and paste the code directly from the other selector, thus defeating the point of using Sass in the first place.

nex3 commented

@Lepidora

This really doesn't make sense, especially if you're trying to style something exactly like another element (trying to style a <input type="button"/>, for example).

This is a really good example. Let's dive into it. Suppose you want to style <div class="button"> exactly like <input type="button">. Let's say you have the following styles:

input {
  border: 2px solid;
}

input[type=button] {
  border-color: blue;
}

Right now, if you write

.button {
  @extend input[type=button];
}

Your <input> will have a blue border, but your <div> won't have a border at all. It's not styled the <input> at all! This is exactly the kind of pitfall the old semantics encouraged, and exactly what we're trying to avoid. If you write instead:

.button {
  @extend input;
  @extend [type=button];
}

You'll end up with a div with a beautiful blue border, just like you wanted.

Maybe an good use example is when you need to adapt/reuse CSS:

i have .button & .button.active:

button { 
    @extend .button;
    &:hover {
        @extend .button.active;
    }
}

I use this a lot. Basically, i depend on this feature to adapt CSS between projects/frameworks (Webflow to WordPress for instance). If this become deprecated, i will need to use an out of date
SASS version...

Your will have a blue border, but your

won't have a border at all. It's not styled the at all! This is exactly the kind of pitfall the old semantics encouraged, and exactly what we're trying to avoid. If you write instead:

I do not see any issues with that. We asked to apply only border-color: blue; by extending specific selector, nothing more. We received the expected result. There are no pitfalls.

Think of it in terms of HTML, where it would work exactly like that:

<input type='email'>
// will have only `border: 2px solid;`

<input type='button'>
// will have `border: 2px solid;` and `border-color: blue;` since it qualifies for both rules

<div class='button'></div>
// will have only `border-color: blue;`, since it qualifies only for second rule.
// It _should not_ look the same way as `input`, since it wasn't our intention.
// If we wanted it to look like `input`, we'd extend `input`

<input type='email' class='button'>
// will have `border: 2px solid;` and `border-color: blue;` since it qualifies for both rules

You'll end up with a div with a beautiful blue border, just like you wanted.

Actually, there is an issue with this solution:

.button {
  @extend input;
  @extend [type=button];
}

It will also grab style like this, while it wasn't our intention:

[type=button] {
  background: red;
}

Thus we wanted to be specific and extend only specified with [type=button] input selector. In other words, only input[type=button]

Your will have a blue border, but your

won't have a border at all. It's not styled the at all! This is exactly the kind of pitfall the old semantics encouraged, and exactly what we're trying to avoid.

I think the most important point is why should you try to avoid anything like that at all.

but this should be a developer's choice. In some cases we cannot edit the selector or transform it in an placeholder.

When we use the current @extend, it works as expected. I still didn't get why change it this way.

100 % agree with @maxviewup about reusing code with @extend. If you are deprecating that because 'extend' the word means something else, then you should provide an alternative to old use-case.

nex3 commented

@maxviewup

i have .button & .button.active:

button { 
    @extend .button;
    &:hover {
        @extend .button.active;
    }
}

If the second extend is just @extend .active instead, this will produce exactly the same output.

@ArmorDarks

I do not see any issues with that. We asked to apply only border-color: blue; by extending specific selector, nothing more. We received the expected result. There are no pitfalls.

The expected result, as originally articulated, was to "style something exactly like another element". @extend input[type=button] doesn't do that. What's worse, it looks like it should, but it may or may not depending on the specifics of how the upstream CSS is written.

By the same token, if the upstream CSS adds a red background to [type=button], the div's background should be red because <input type="button">'s background will be red. That's what "exactly like another element" means.

@khurshid-alam

If you are deprecating that because 'extend' the word means something else, then you should provide an alternative to old use-case.

We have an alternative: factoring the shared styles into a mixin.

If you can't do this because you don't control the upstream styles, you're already in trouble: the upstream author may make a change that looks safe to them but breaks you. Suppose they changed:

input[type=button] {
  font-weight: bold;
}

to:

input {
  font-weight: bold;
}

They would believe that their buttons would still be bold. But suddenly your divs, which are supposed to look like buttons, aren't bold anymore. This is why breaking CSS semantics is such a problem: it breaks the ability to reason about what effect style changes will have in isolation.

If the second extend is just @extend .active instead, this will produce exactly the same output.

It will grab all .active rulesets, isn't it? Not only .button.active... Specificity.

The expected result, as originally articulated, was to "style something exactly like another element". @extend input[type=button] doesn't do that. What's worse, it looks like it should, but it may or may not depending on the specifics of how the upstream CSS is written.

I think that's where the confusion comes. Nobody expects extend to result in "style something exactly like another element". It is expected to "apply all rulesets, which qualifies for extension rule, to this ruleset".

If you can't do this because you don't control the upstream styles, you're already in trouble: the upstream author may make a change that looks safe to them but breaks you. Suppose they changed:

I'd vote for this advise. But it is more in the sense that mixins are a generally better solution than extends and that extension maybe even should be dropped completely.

nex3 commented

It will grab all .active rulesets, isn't it? Not only .button.active... Specificity.

Sorry, yes, this is true.

I think that's where the confusion comes. Nobody expects extend to result in "style something exactly like another element". It is expected to "apply all rulesets, which qualifies for extension rule, to this ruleset".

That's not how @extend has ever been documented or explained.

That's not how @extend has ever been documented or explained.

I'm really trying hard to get the logic. So far we're going to the point that @extend is some very weird form of @include.

As I've stated above, I even do not care about @extend much, because with proper usage of @include it is mostly not crucial feature. So, my involvement in that conversation is slightly odd. But since it still stays in Sass, I'd really like to get why exactly it works that way.

But so far I can't. All examples were quite strange to me and left strong feeling that there is a design flaw in the current form of @extend. I do not deny that it might happen because I miss some point. But current situation clearly shows that community can't get it either.

nex3 commented

I think one issue a lot of people have with understanding @extend is that they think about it in terms of what it does to the CSS output. It's much better to look at it from the perspective of how your HTML is styled: it causes all selectors that match one selector to be styled as though they also match another selector.

@nex3

I am still unable to solve this:

    %linked_button { 
        border-width: 1px;
        &:first-child { border-width: 0; }
    }

a { @extend %linked_button:first-child; }

It used to produces

a {
  border-width: 0;
}

What should I do now? Does it make sense to deprecate extend for placeholder selectors as well? Placeholder selectors, in the first place, are created to be used like that.

I think one issue a lot of people have with understanding @extend is that they think about it in terms of what it does to the CSS output. It's much better to look at it from the perspective of how your HTML is styled: it causes all selectors that match one selector to be styled as though they also match another selector.

Yes, that explanation sounded, and not once. In #1599 (comment) I've shown how it doesn't apply well exactly in terms of HTML. Specificity still works there, the same way as it does in CSS itself, and as it could work in @extend. I can't get why specificity has been thrown here completely into the window.

@khurshid-alam

Right now the only solution would be to extract %linked_button:first-child content into mixin and use it in both places:

@mixin link-style() { border-width: 0 }

%linked_button { 
  border-width: 1px;
  &:first-child { @include link-style(); }
}

a { @include link-style(); }

Or in your case just of the variable would be enough:

$link-border-width: 0;

%linked_button { 
  border-width: 1px;
  &:first-child { border-width: $link-border-width; }
}

a { border-width: $link-border-width; }

Right now the only solution would be to extract %linked_button:first-child content into mixin and use it in both places...

@ArmorDarks in cases that you have power to create mixins and reusable rules correctly, this is great and is the right thing to do.

But i'm worried about cases that you can't do this (legacy css or generated CSS).

I use extend most on this case. basically, since i've discovered extend i use it this way: Extend an CSS Rule.

That's why for me is great, i can pick any CSS and fix for my needs. Any Framework, Legacy project, without changing the code itself.

This may be not the best way to do it, but is the most reliable because supports base project changes.

-1 for this suggestion, it will break my framework: https://github.com/esr360/Synergy/wiki/Sass-Mixin-%E2%80%93-Extend

As per this example: https://github.com/esr360/Synergy/wiki/Sass-Mixin-%E2%80%93-Extend#use-within-another-module

DEPRECATION WARNING on line 2148 of ../../Synergy/dist/_synergy.scss:
Extending a compound selector, [class*="list-"][class*="-reset"], is deprecated and will not be supported in a future release.
See https://github.com/sass/sass/issues/1599 for details.

@extend .a.b should only extend instances of .a.b... if you want both .a and .b why not have a syntax like @extend .a, .b ? I just don't understand this change.

nex3 commented

@esr360 The idea of "instances of .a.b" doesn't make sense from the perspective of CSS. .a.b is a selector, which means it matches a set of elements--specifically, elements with class="a b". Those elements are styled not just by .a.b { ... }, but by .a { ... } and .b { ... }. If you want to style other elements as though they matched .a.b, you need to take into account all styles that apply.

In your case, I suspect your extend mixin would work fine if you wrote @extend [class*="#{$selector}#{$modifier-glue}"], [class*="#{$modifier-glue}#{$modifier}"]. In fact, it looks like this is already what's happening when $modifier is a list:

        @else if type-of($modifier) == 'list' {

            $selectors: if($parent,
                ('[class*="#{$namespaced-parent}#{$modifier-glue}"]'),
                ('[class*="#{$module}#{$modifier-glue}"]')
            );

            @each $item in $modifier {
                $selectors: join($selectors, '[class*="#{$modifier-glue}#{$item}"]', comma);
            }

            @extend #{$selectors};
        }

@nex3 The difference between:

@extend [class*="list-"][class*="-reset"]

And:

@extend [class*="list-"], [class*="-reset"]

Is that the first will only extend the reset modifier styles belonging to the list component, which is precisely what I want, I'm not interested in the core component styles.

This isn't supposed to map to the HTML at all, because my HTML might be class="list-inline-reset. It's a compound selector, yes, but it targets just one class.

As per this example it lets me extend multiple modifier styles into a new modifier:

@include module(button) {

        @include modifier('round')   {...}
        @include modifier('large')   {...}
        @include modifier('success') {...}

        @include modifier('primary') {
                @include extend(('round', 'large', 'success'));
        }
}

And from there I can extend the styles into new components:

@include module('cta') {
    @include extend($parent: 'button', $modifiers: ('round', 'success'));
}

In the above example I then pass the core styles using a flag:

@include module('cta') {
    @include extend($parent: 'button', $modifiers: ('round', 'success'), $core: true);
}

I can now use the following HTML to achieve the same thing:

<div class="button-round-success">...</div>
<div class="cta">...</div>

I don't see how this behaviour can be maintained with this new change.

I changed it to @extend [class*="#{$selector}#{$modifier-glue}"], [class*="#{$modifier-glue}#{$modifier}"] like you suggested, and it removed the warning but it completely changes my output, bloating it up:

https://imgur.com/a/rss0Q

Taking it from 142kb to 201kb...

I'm still hoping that maybe I'm missing something...

The newest version of sass already stop to warning and is giving error. Many of my projects are/will break. I'm downgrading to 3.5.1, that just give warnings...

Is there an alternative besides downgrade? many projects like ours will break with this.

Ok, so there's been a lot of back and forth and for whatever reason, just @extending .a.b doesn't make sense, fine.

For some people, there is the option of restructuring their code so it still works. However, it seems like many people are building off of an immutable codebase or are otherwise unable to change the existing code.

There is no escaping the fact that this change has made certain things that were previously possible with Sass impossible. .a.b is a valid selector in CSS. There is no reason that functionality in Sass should arbitrarily not work for certain CSS selectors.

Because of that, what are Sass's plans moving forward to let users take CSS code from a compound selector and put it into another selector?

I'm pretty sure node-sass struggles with extending compound selectors. My app, which extensively uses extending compound selectors for legitimate reasons, has never compiled properly even with the latest versions of node-sass, I've always had to use Ruby Sass. Given that node-sass is the most popular implementation of Sass, I bet they have pressured the Sass developers to just remove the feature from Sass altogether to ensure backwards compatibility. That's my theory.

nex3 commented

@esr360 Your extend() mixin already produces comma-separated selectors when $modifiers is a list, so the behavior can't be that bad.

The newest version of sass already stop to warning and is giving error. Many of my projects are/will break. I'm downgrading to 3.5.1, that just give warnings...

Which version is this? Version 3.5.5, the most recent, doesn't give an error.

I changed it to @extend [class*="#{$selector}#{$modifier-glue}"], [class*="#{$modifier-glue}#{$modifier}"] like you suggested, and it removed the warning but it completely changes my output, bloating it up:

https://imgur.com/a/rss0Q

Does this actually change the way your HTML is styled, or does it just add selectors that don't end up matching anything?

@Lepidora

.a.b is a valid selector in CSS. There is no reason that functionality in Sass should arbitrarily not work for certain CSS selectors.

.a.b is indeed a valid selector, and what it means is "the set of elements that have class="a b"." If you want to style an element as though it matches .a.b, that would be equivalent to adding class="a b", which means it should be affected by style rules for .a and for .b as well as for .a.b. That's not what @extend does currently, which is why we're deprecating the current behavior. It is what @extend .a, .b does which is why we're suggesting people move to that instead.

Because of that, what are Sass's plans moving forward to let users take CSS code from a compound selector and put it into another selector?

This is not a use-case we intend (or ever intended) to support. It's never safe to do this, because CSS says that <div class="a b"> should be styled exactly the same with

.a.b {color: blue}

as with

.a {color: blue}

and this breaks that invariant.

I still can't get the point here. In CSS when you write:

.a.b {color: blue}

You get .a.b styled, .a and .b are left untouched. Why shouldn't be the same with @extend?

@nex3, it turns out the issue my app didn't compile with node-sass was because of this:

sass/libsass#2520

So I guess that proves my theory wrong!

Anyway, the example image I provided, it adds a TON of selectors that do not match anything. I'm not sure what's going on, but obviously the way I, and others, have been using @exted and the way you think people should be using it do not align.

It all boils down to this. Consider I have this style rulseset:

[class*="list-"][class*="-reset"] {
    list-style: none;
}

This is a single compound selector, and it will match all of the following:

<ul class="list-reset">...</ul>
<ul class="list-inline-reset">...</ul>
<ul class="list-inline-reset-arrow">...</ul>

Consider I have another element:

<div class="social-links">...</div>

I want to achieve the following, but without adding the new class:

<div class="social-links list-reset">...</div>

In my Sass, I should be able to do:

.social-links {
    @extend [class*="list-"][class*="-reset"];
}

I'm not sure I really understand what I'm supposed to do now to achieve this behaviour.

@nex3 the problem about this change is that SASS think you have control about all the project (HTML/CSS) and someway is trying to tell how to do your project (you shouldn't using compound selectors blabla) and not focusing on features. Like any language, is there an right way and an wrong way to do things. I recognize that this practice isn't good if you can avoid it but in a lot of cases you doesn't have much choice.

If this change is about semantics (as the title says), it should remove something that is really used in a lot of projects?

Anyway, i recommend using an older version of sass to keep it working, and if you're using node-sass, i don't know how to help...

@maxviewup I agree extending compound selectors is 99 times out of 100 bad practice, but even in a project where you do fully control the HTML/CSS, there are still legitimate reasons to extend compound selectors, such as in my example.

I can't stress enough that this change breaks the functionality of https://github.com/esr360/Synergy and hence https://github.com/esr360/One-Nexus, which relies specifically on extending compound selectors. I can deprecate some of the functionality, or I can lock them to an older version of Sass. At the end of the day, these are my choices. There is no work around for my use case.

@esr360 i'm on the same situation and for now, i think that sass will not change back.

But maybe the team can create another functionality that reproduces the old extend... That certainly should solve the problem (an simple string replace to fix an project is better roll back to an older version).

I agree extending compound selectors is 99 times out of 100 bad practice

It is not a bad practice as long as .a.b, .a, .b or even a[title] are all separate and valid selectors. Using styles from .a.b does not mean we should also apply styles from both .a and .b.

I believe the source of this change is somewhere else, like if someone decided to keep the code neater and removed the functionality that prevented from doing so.

@heaven The source may be the one on the issue:

according to the semantics of @extend. We should fix this, especially as CSS moves closer to supporting @extend natively.

So native CSS support may be different from the sass

@maxviewup true, but it would be fair to deprecate such thing when we know for sure how will the native @extend work. And even after that, it would take a few more years before we'll be able to use it natively.

@nex3

You're missing something out. You assume that both .a and .b are defined styles. It is perfectly valid to have styles .a and .a.b without having a .b style. So the selector .a.b doesn't actually say for certain that the element has both .a and .b styles applied to it.

You could have a stylesheet with style1 and style2 as different classes and gradient to act as a modifier for the styles. Even though .gradient on its own wouldn't do anything, .style1.gradient would be valid, as would .style2.gradient.

Then what do you do if you want to use the contents of .style1.gradient in another class?

Heck, you don't even need a .a or a .b. You could have something only apply a style on .a.b. .a and .b wouldn't get applied because they don't exist.

@Lepidora I feel like you're talking to a brick wall as this point has been raised several times already.

Heck, you don't even need a .a or a .b. You could have something only apply a style on .a.b. .a and .b wouldn't get applied because they don't exist.

100% correct - just like my example I gave a few posts up:

[class*="list-"][class*="-reset"] {
    list-style: none;
}

To target:

<ul class="list-reset">...</ul>
<ul class="list-inline-reset">...</ul>
<ul class="list-inline-reset-arrow">...</ul>

As you pointed out, <div class="list-"> will never exist, nor will <div class="-reset">. I don't get why the Sass developers are almost being willfully ignorant surrounding this issue.

Oh, my. There was a lot of discussion about how exactly should @extend work, and I wasn't able to get @nex3 position at all.

But suddenly it stroke me.

It doesn't change a lot, but hopefully, it will help people like me to get what's happening. @nex3, please, correct me if I'm wrong.

So, what you're saying, Sass treating @extend like it's extending not CSS, but HTML? In other words, it works exactly like document.querySelectorAll(), and applies extender to all returned values. Right?

So, that is why when we say:

.foo { border: 0; }

a.foo { border: 0; }

.foo.test.me { border: 0; }

.bar { @extend .foo; }

we expect to receive

- .foo { border: 0; }
+ .foo, .bar { border: 0; }

  a.foo { border: 0; }

  .foo.test.me { border: 0; }

- .bar { @extend .foo; }

but instead receiving

- .foo { border: 0; }
+ .foo, .bar { border: 0; }

- a.foo { border: 0; }
+ a.foo, a.bar { border: 0; }

- .foo.test.me { border: 0; }
+ .foo.test.me, .test.me.bar { border: 0; }

- .bar { @extend .foo; }

It is because if we will make counterparts of those selectors in HTML, we'll get this:

<div class='foo'></div> <!-- .foo -->
<a class='foo'></a> <!-- a.foo -->
<div class='foo test me'></div> <!-- .foo.test.me -->

and if we will ask DOM with document.querySelectorAll('.foo') (which corresponds to the way how Sass treats extending), we will get not only <div class='foo'></div>, but all of those nodes. Because they all respond to .foo selector. And thus Sass tries to style them all, to reproduce such behaviour.

Oh, that was hard for me. Sorry for taking so long.

However, I don't see how compound selectors violating any semantics here. Once again, document.querySelectorAll() is a perfect representative of what we're trying to achieve since it closely mimics the behaviour and allows us to get how should it work. And using it with compound selectors will work exactly the way it should: document.querySelectorAll('a.foo') will return the only node, <a class='foo'></a>. Sass should make it so too, by extending only qualified selector a.foo and everything that responds to it, like a.foo.test or body a.foo, but not .foo.


So, it was said so much about CSS semantics and how that or another approach violating it. But after getting into how @extend works, I'm not sure that @extend has anything to do with CSS semantics at all. In facts, it's based solely on HTML semantics. And that's where the confusion comes from.

But the real issue here that I'm no longer sure that @extend behaviour is legit. The fact that it treats CSS as a subset of HTML is questionable. This means, that it ignores one of the most important specs of CSS β€” qualifying, and relies on assumption that CSS will be used as part of HTML. That leads to the situation, that instead of identifiers (selectors), with each having qualifying value, Sass see only set of elements, like it do HTML: instead of a.foo it sees elements a and .foo, instead of .foo.test.me it sees .foo, .test, and .me.

I'm not sure that it is right because this isn't what CSS is. It is how CSS treated by HTML. And what is worse, HTML is not the only consumer of CSS. Some other consumers can treat CSS differently, and by treating CSS only the way HTML treats it, we're effectively breaking the promise of a predictable system to other consumers.

I think that we should think of CSS only in terms of CSS. And in such case, extending should take into account qualification and extend selectors, which are matching that qualification only in terms of CSS:

.foo { border: 0; }

a.foo { border: 0; }

.foo.test.me { border: 0; }

.bar { @extend .foo; } // should extend only `.foo`, since it is the only matching rule here

In the current situation, I'd wish that there were at least kind-of "precise" @extend, which would work that way. Like this:

.bar { @extend($strict: true) .foo; }

Though, I have very little doubts that @extend will be ever reviewed. Too late, probably...

.bar { @extend($strict: true) .foo; }

Would actually solve all my needs, good suggestion. I can't understand why an in-demand feature is being ignored.

I see both sides of the argument, and recognize that @extend doesn't currently work as originally intended, though it seems there are many projects that would be affected, and many developers who wish to retain the existing functionality. Granted, there are examples above that could lead developers to shoot themselves in the foot, but as developers we are confronted with this reality on a daily basis and take precautions to ensure we do not.

Simply removing the functionality, unfortunately, does not create an easy migration path for codebases that rely on this functionality, especially ones with a large CSS footprint. Allowing a strict option, as in @extend($strict: true) would solve that, but may cause confusion by overloading @extend. I believe the ideal solution would be to introduce another keyword (@implements?) that incorporates the old behaviour.

.bar { @implements .foo; }

I think most would be more than happy running a quick sed script and going about their day:

find . -name *.scss | xargs sed -i '' 's/@extend/@implements/g'

It annoys me that this (significant) change was made on the whim of someone writing a bug report. I feel for something with such widely used effects, it should be thoroughly discussed with the Sass developers and the community before any change is made.

I know I for one will be moving away from Sass in the future because I now have no idea what features could have their functionality arbitrarily changed.

So....can we get some sort of update? Will anything be considered to cater for those of us who still need the old functionality? There have been some good suggestions so far...

Hi everyone, I'd like to chime in here. For context, @extend was my idea, but when I initially proposed it, it only allowed extending simple selectors. As the design of the feature progressed, @nex3 realized that there was a more general definition that theoretically worked for compound selectors and she and I worked together to define the semantics of how that would work.

I want to be very clear about a few design goals of @extend. The primary design goal was to couple one class to another in CSS such that in markup, there was no longer a need to force a developer to apply several classes in conjunction. The definition of extend has always been about the net effect on markup.

To be honest, @extend was a huge experiment on our part, we knew that it had some gaps between the definition and the implementation (as noted in this thread, sometimes the cascade resolves incorrectly due to specificity issues) and we hoped that it would pave the way to a native implementation in CSS itself, which would not have those issues.

Historically, @extend has been the most problematic feature in Sass. So much so, that many projects use linting to forbid it from being used at all. Many developers do not grok how it should work and even when they do, they can still end up generating output that is far more complex than initially expected. With a heavy heart, when I defined the sass coding guidelines for linkedin, I too decided @extend was not safe for use on our styles at scale.

The complexity of this thread demonstrates the magnitude of how misunderstood the feature is by many developers. Much discussion has happened over the years to imagine ways of mitigating issues with extend.

I will point out that @nex3 has been a faithful maintainer of Sass for over a decade now. Natalie decided that porting to Dart was the path to creating a situation where she'd want to continue maintaining the project instead of simply abandoning the ruby implementation.

She and I decided, together, that the cost of porting a very complex feature with known issues (which she has patiently tried to explain in this thread) was not worth it. And, yes, we realized that this would affect users. First and foremost: you don't have to upgrade. Lock down your sass version in your Gemfile. There's no more new features coming to the ruby implementation, so you're not missing out on anything super important. Downgrade to before the deprecation and maintain your application's current working order. You were given free software that worked for you and it continues to do so at the version you had. The deprecation warnings are meant as a path to help ruby users port their stylesheets to the dart implementation.

The Dart implementation will not be getting an @extends that works as it did in ruby. The decision to reduce the surface area of this misunderstood aspect of the feature is a good one for the long term. And @nex3 went out of her way to add capabilities to the ruby implementation to detect and warn about this incapability -- That is a responsible and appropriate way to remove a feature. You're welcome. If you're decision is to stop using Sass because of this, so be it. But I will say that Sass has a decade of history of providing robust and well thought out designs. If you think it's operating on whims, you are very mistaken. Our record speaks for itself. Removing this feature that is quite misunderstood actually speaks volumes to the degree to which, we believe that the features Sass provides should be intuitive and robust against developers who do not fully understand implementation details and have not fully read the documentation we provide.

If you're using this feature and you want to port to Dart Sass, you have the following options:

  1. Extract the styles to a placeholder and extend it in both the original location of those styles and in the location where you used to extend the compound selector.
  2. Similar to above, you can extract a mixin and include it into both locations.
  3. You can remove your extend statement altogether and update your markup to match the original styles in conjunction with the additional styles it matched.

We understand that this is work and that it's annoying to do. We understand you're probably not happy about it and that you may be frustrated with this decision. As stated above, you only need to do this work IF you intend to port to Dart Sass. That's not a simple task, it will affect your build scripts, your app's code, and you should allocate an appropriate amount of time to do the work in accordance with your business's priorities.

Disappointing news. Technology should evolve to cater for the needs of the users, not the other way around. But you're absolutely right, we get to use Sass for free, so far be it from us to complain. Sass is fantastic with or without this feature.

@esr360 this is free and open source software. Feel free to fork and maintain.

@jdurand that's a very impractical solution and not how open source should really work.

efojs commented

@nex3, thank you for your patient explanation, and @chriseppstein for your long-read, and for Sass.

Extract the styles to a placeholder and extend it in both the original location of those styles and in the location where you used to extend the compound selector.

β€” this is what (who tbh did not read docs) I was looking for β€” solution for my hovered bar
.bar:hover { .button { @extend .a:hover; }}, in same case as @ArmorDarks mentioned:

It was useful, when element in unhovered state had to look like exactly as another hovered element.

You are doing great job!

binki commented

I find myself agreeing with the logical argument of the OP.

However, I had written a bunch of code relying on the old behavior. Because I have control over my project, I am simply adding new placeholders next to each compound selector I want to be able to extend. I think this is the easiest way to transform massive amounts of existing code as it doesn’t require one to to understand how it works to port to sass-4:

Original (order changed so that specificity plays a role in the output, that @extend .a; @extend .b would result in an unwanted change in behavior):

.a.b {x: c}
.a {x: a}
.b {x: b}

.c {@extend .a.b}

On sass-4-pre, this is an error. The output of dart-sass:

Error: expected selector.
.c {@extend .a.b}
              ^
  - 5:15  root stylesheet

Modified:

%a-b, .a.b {x: c}
.a {x: a}
.b {x: b}

.c {@extend %a-b}

Output (manually slightly compressed):

.c, .a.b { x: c; }
.a { x: a; }
.b { x: b; }

The downside of this is that you have to remember to add the % selector to every instance of the particular compound selector you’re targeting. You cannot simply do a text search because SASS-3 at least knew that extending .a.b would also match .b.a and .a.c.b, for example.

Sorry to revive this but there are still use-cases in these comments that are unaddressed and suggestions which have been ignored and I still require the ability to extend compound selectors and have so far not been convinced that my use-case is not legitimate.

Rather than ignoring the users who still require this functionality, could the developers instead try to understand our position and come up with some solution or better educate us on why our use cases are not legitimate so we no longer seek a solution?

As @binki mentioned, the only apparent way to refactor my code to accommodate this change is to move all my compound selector styles into a placeholder.

Whilst this is fine, it still seems like a step which should be unnecessary for me to take.

Please, help me, this Dart Sass version not work @extend .class1, .class2;
Necessary coding @extend .class1; @extend class2;

How to proceed? This is correct? This (@extend .class1, .class2; ) is deprecated?

nex3 commented

@tassiogoncalves @extend .class1, .class2 should work in Dart Sass. Can you file an issue?

combs commented

This is a loony change and the deprecation warning cites a dead URL: See http://bit.ly/ExtendCompound for details.

nex3 commented

@combs That tone is not acceptable. Please follow the community guidelines or you will be blocked from commenting on Sass repositories.

combs commented

Sorry. I meant to point out that the deprecation warning cites a dead URL: See http://bit.ly/ExtendCompound for details.

nex3 commented

Thank you for the report. It looks like the JavaScript we set up to redirect anchors in the new documentation isn't working. I'll look into it.

@nex3 please look at this simplified example that uses a compound selector and suggest the correct way I should be doing it:

https://codepen.io/esr360/pen/zQRZdY

The same code on SassMesiter throws an error, and the "suggestion" from the error throws another error...not a particularly good developer experience.

nex3 commented

@esr360 I would change @extend [class*="button-"][class*="-round"] to @extend [class*="button-"], [class*="-round"], which produces the same output and accurately represents the idea that .myButton is a button and is round. This works exactly the same as if you had written <div class="button-round myButton"> in your HTML instead of writing the @extend.

It's also worth noting that the comments on your styles are contradictory. You say that all buttons should be styled with the .button styles, but then you say that .myButton should only extend the round button styles. Is it a button or isn't it? This kind of semantic confusion is exactly the problem with extending compound selectors.

@nex3 I really appreciate your response.

Actually you can see that whilst in this particular case the net effect is the same, the CSS output is in fact different:

.button, [class*="button-"], .myButton {
  display: block;
  background: red;
}

[class*="button-"][class*="-round"], .myButton {
  border-radius: 4px;
}

.myButton {
  display: inline;
  background: blue;
}

...vs the original:

.button, [class*="button-"] {
  display: block;
  background: red;
}

[class*="button-"][class*="-round"], .myButton {
  border-radius: 4px;
}

.myButton {
  display: inline;
  background: blue;
}

The difference being that myButton in the original case does not extend the .button styles. The display property in the updated suggestion (https://www.sassmeister.com/gist/2dbeb5ed3178a47d3de0319050776167) will now be overwritten. If .button were to contain any properties that .myButton did not, .myButton would incorrectly extend these properties as well, but in reality we just want to extend the border-radius property.

I take your point on board about semantics, the example I provided doesn't do a good job at showing the problem in a practical sense, only a logical one, so please consider the same question to this revised example:

https://codepen.io/esr360/pen/zQRZdY

Can you suggest a correct way to achieve the above?

binki commented

@esr360 Using the placeholder selector technique I described in an earlier comment, you can get identical output. I have forked your codepen here: https://codepen.io/apostrophe/pen/gJKBbm .

I tried to determine if this pattern would be compatible with your code base. You appear to be using pattern matching to incrementally accumulate rules onto an element. However, in defining [class^="button-"][class*="-primary"] , your use of @extend created a new rule set in a way that didn’t participate in that pattern-matching-based rule accumulation. Thus, I don’t think the use of placeholders is a step backward for the example you provided.

You appear to be using pattern matching to incrementally accumulate rules onto an element.

@binki thanks for much for actually understanding what it is I'm trying to do - this is exactly right.

Your solution using placeholders seems like it could work, though it will require my framework to be re-written, and will still feel like a "hack" to get around this change, which I'm still struggling to see the logic of.

nex3 commented

I take your point on board about semantics, the example I provided doesn't do a good job at showing the problem in a practical sense, only a logical one, so please consider the same question to this revised example:

https://codepen.io/esr360/pen/zQRZdY

Can you suggest a correct way to achieve the above?

This one's actually easier: just get rid of [class*="button-"] in your extends. Because the extending selector already includes [class*="button-"], Sass's intelligent unification will avoid duplicating that selector. See my forked pen to see it in action.

@nex3 that's very interesting! At first glance, it seemed like the code would break if I introduced some other style that had [class*="-round"] (e.g. [class*="icon-"][class*="-round"]), since all [class*="-round"] cases would be extended, and whilst it generated the following CSS:

[class*="icon-"][class*="-round"], 
[class*="icon-"][class*="button-"][class*="-primary"] {
  border-radius: 50%;
}

it shouldn't break anything. Ideally it wouldn't be in my output because I'm really just wanting to extend [class*="-round"] for buttons only. In my framework, any component that has round modifiers will extend button into the selector, but it won't break anything I don't think so I suppose this is acceptable.

Thanks very much for putting time in to help me.

nex3 commented

In theory, you could have an element with class="icon-button-primary" that should have that border-radius. You could rewrite your selectors to be [class^="icon-"] and [class^="button-"], in which case we could potentially detect that they're mutually incompatible during intelligent unification (currently we don't have that logic but it wouldn't be too hard to add).

@nex3 Hi, I was wondering if you could clarify something for me, as I'm trying to resolve these deprecation warnings.

You can use compound selectors to define new rules that are present in neither of the composing selectors individually, like in this CodePen where .popover is combined with .top https://codepen.io/denholms/pen/MWYavzm.

You say "If an element matches .a.b, that means it also matches both .a and .b individually", but aren't you describing ".a .b", not ".a.b" where the classes are used simultaneously?

lwxbr commented

Please, @nex3, just create a flag ignoring @extend strict rules and let us compile grouped selectors. For example:

$ sass --ignore-extend-rules <args>

Okay, If that breaks the rules of 'extend' and it can be unbearable to do, lets create another keyword to fit the a.b cases.

nex3 commented

@dscrimshaw

@nex3 Hi, I was wondering if you could clarify something for me, as I'm trying to resolve these deprecation warnings.

You can use compound selectors to define new rules that are present in neither of the composing selectors individually, like in this CodePen where .popover is combined with .top https://codepen.io/denholms/pen/MWYavzm.

You say "If an element matches .a.b, that means it also matches both .a and .b individually", but aren't you describing ".a .b", not ".a.b" where the classes are used simultaneously?

If an element matches .a .b, that means the element matches .b and it has a parent that matches .a. If an element matches .a.b, that means it matches both .a and .b individually. For example, in this CodePen:

  • The p element matches .border.color (you can tell because it has a red background).
  • The p element matches .border (you can tell because it has a black border).
  • The p element matches .color (you can tell because the text is blue).

@lwxbr We don't add flags that affect compilation behavior for many reasons. It adds complexity for every user, it increases our maintenance overhead, and worst of all it means that Sass stylesheets compile to meaningfully different CSS in different contexts.

Also, we don't consider the ability to extend compound selectors to be desirable behavior for anyone. I understand why people are using it as a convenient shorthand for manually writing out the selectors they need, but that shorthand almost universally produces more confusion for future readers of those stylesheets than it saves in the short term. It's not something we want to encourage.

@nex3 Thanks for the clarification.

If you had the intent to extend the rules of .border.color though, for instance (and sorry if this is a poor example - it's the end of my working day) if you're using border as a styling class and color as representing a state, and you're extending these rules on a main page to something that's now in a modal window or something, as far as I understand you now can't? Seems like you have to define a new class for that styling+state scenario to then extend?

nex3 commented

Can you give a more concrete example?

lwxbr commented

Also, we don't consider the ability to extend compound selectors to be desirable behavior for anyone.

@nex3, this issue is definetely a wontfix issue, I suggest you tag it as it really is.

I think the fair solution for this case is let the users solve this by themselves. For example: I don't will implement that feature in the core of the sass, but you can write a plugin if you want and distribute it to others if that might help them instead of fork sass and make another sass compilers, etc, etc.

But saying like "that is it and period" will keep this issue and others in this state of unsolvable problems.

Currently, when a compound selector is extended, Sass will only replace instances of the full extendee with the extender. For example:

.a {x: y}
.b {x: y}
.a.b {x: y}

.c {@extend .a.b}

produces

.a {x: y}
.b {x: y}
.a.b, .c {x: y}

when it should produce

.a, .c {x: y}
.b, .c {x: y}
.a.b, .c {x: y}

This is one of the reasons i stopped using sass. This feature was the best one of the tool. And just because someone didn't like the way it was implemented, it was removed to everyone (and nowdays is almost impossible to use it). I used a lot this feature @extend .a.b to integrate with old layouts and extend styles. Today, if you didn't apply an strict pattern such as BEM, you are scrolled.

You coud just achive it in this example by just using:

.a {x: y}
.b {x: y}
.a.b {x: y}

.c {@extend .a, .b}

but,

NO

everyone need to loose this feature.

How do i supose to fix this case:

// base
.foo {
    .bar {
        .button {
            background: red;
            color: white;
        }
    }
}

// extending
.my-link {
    @extend .foo .button;
    border: 1px solid yellow;
}

whithout changing the base code? it's impossible. Thank you πŸ‘Œ

but that shorthand almost universally produces more confusion for future readers of those stylesheets than it saves in the short term. It's not something we want to encourage.

@nex3 you are not encouraging, you are literally FORBIDDING anyone to use this feature. As a developer, i like to make choices about my project and decide myself if something is or isn't suitable or right to do, and not let someone else decide for me. If it was really an recommendation, it would only generate an warning, not an error.

This is a useful feature and not just an good practice. There is a lot of projects that may take advantage of this, like

  • Extending another project / theme style
  • Supporting an legacy project style
  • Extend/modify behaviors when you are not allowed to change the base code (using an elements library such bootstrap for instance)
  • Extend/modify generated CSS/SCSS without changing the source code (eg an generated css code from webflow that may be generated again)

If still isn't "right" to use it in @extend, just please create a way to do so. As @lwxbr , an flag or even an plugin to sass. Or, create another tag such as @inject or somehting like it.

This behavior is only usefull when you start an scss project from scratch, and using an strict pattern like BEM. But when you extend another layout, or start from an base such as bootstrap, this is only an bad limitation

Replacing @extend a.b with @extend a, b might be semantically identical but it produce a bloated output in my case. I have a sass library that customizes bootstrap.

For example, I have a rule that is like

.navbar-nav .active .nav-link {
   @extend .nav-link.active; 
}

Which is meant to make selector .navbar-nav .active .nav-link extend the styles from selector .nav-link.active(very few lines of styles). output css has around 19k lines.

But if I use @extend .nav-link, .active; or @extend .active; , the output has 250k lines of css. Since .active and its combination selectors has a lot of styles, output contains a lot of styles rules that is not expected to exist.