Create a gist now

Instantly share code, notes, and snippets.

@nex3 /gist:8050187
Last active Jan 19, 2017

A Change in Plans For Sass 3.3

Sass 3.3 is coming soon, and along with it several major new features. It supports source maps, SassScript maps, and the use of & in SassScript. In preparation for its release, we've put out a couple of release candidates to be sure that everything was set and ready to go. Unfortunately, it wasn't.

Release candidates often turn up small bugs and inconsistencies in new features, but it's rare that they find anything truly damning. In this case, though, several users noticed an issue with using & in SassScript that rendered a sizable chunk of our plan for that section of 3.3 unworkable. It's not a fatal issue, and we think we have a good plan for dealing with it (I'll get to that in a bit), but it is a problem.

The Background

To understand what's wrong, first you need to understand the reason we decided to make & accessible to SassScript in the first place. One thing users want to do pretty often is to add suffixes to classes. Sometimes this takes the place of nesting selectors, sometimes it's just to make a new class based on the old ones -- the reason doesn't matter much to this discussion. When people tried to do this, they'd write something like .foo { &-suffix { ... } }, and it wouldn't work. The reason is that & has the same syntactic function as a type selector (e.g. h1) or a universal selector (*), since it could be replaced by any of those. It doesn't make sense to write *-suffix in a selector, so &-suffix wasn't allowed either.

This didn't stop people from wanting to do it, though. So we decided, "all right, we already use interpolation (#{}) to support injecting text into selectors -- let's just use that". We decided to add & as a sort of special variable in SassScript that contained a parsed representation of the current selector. You could then mimic &-suffix by doing @at-root #{&}-suffix instead¹. Life was peachy, until our intrepid users discovered the problem.

The Problem

Here's a small snippet of SCSS that demonstrates the issue. See if you can figure it out:

.foo, .bar {
  @at-root #{&}-suffix {
    color: blue;
  }
}

Did you get it? That's right: & in this example is .foo, .bar, which means the selector compiles to .foo, .bar-suffix. Since #{} injects plain old text, there's no chance for Sass to figure out how it should split up the selector.

Chris and I talked and talked about how to fix this. We considered adding a function to add the suffix, but that was too verbose. We considered making & split the compilation of the CSS rule into several parallel rules which each had a single selector for &, but that was too complicated and fell down in too many edge cases. We eventually concluded that there was no way for SassScript & to cleanly support the use case we designed it for.

The Solution

We knew we wanted to support the &-suffix use case, and our clever plan for doing so had failed. We put our heads together and discussed, and decided that the best way to support it was the most straightforward: we'd just allow &-suffix. This was, after all, what most people tried first when they wanted this behavior, and with the & embedded directly in the selector, we can handle selector lists easily.

This means that &-suffix will be supported in Sass 3.3, without needing #{} or @at-root. I've created issue 1055 to track it. When compiling these selectors, if the parent selector is one that would result in an invalid selector (e.g. *-suffix or :nth-child(1)-suffix), we'll throw an error there describing why that selector was generated.

We are still worried about cases where people write mixins using &-suffix that will then fail to work with certain parent selectors, but in this case we determined that this would be the least of all available evils.

The Future of & in SassScript

In addition to supporting &-suffix, we've decided to pull SassScript & from the 3.3 release. Rest assured that it will return -- we recognize that it has other good use cases, and we intend to bring it back for the next big release (likely 3.4). In addition, it will come with a suite of functions for manipulating the selectors it makes available, so it will be more powerful than ever.

There are two reasons that we want to hold off on using & in SassScript for now. The first is that we want some time to create the functions that will go along with it and put them through their paces. This may require changing the way it works in various ways, and we don't want to have to make backwards-incompatible changes to do so.

The second reason is that we've spent a fair amount of energy talking up #{&} as a solution to the &-suffix problem. This is our own fault, clearly, but it's true and it's something we need to deal with. Making &-suffix work is great, but if everyone is using #{&} anyway because that's what we told them about a few months ago, then it's not doing everything it can. Having a release where &-suffix works but #{&} doesn't will help guide users towards the best way to solve their problem, before we make the more advanced functionality available.

@at-root will still be included in Sass 3.3.

Releasing 3.3

Unfortunately, this change will delay the release of 3.3, but hopefully not by too much. I anticipate this being relatively straightforward to implement; the major hurdle was figuring out what to do about it, and that part's done. I plan to devote a large chunk of time to getting 3.3 out the door after I come back from winter vacation, so hopefully (no promises) it'll be released some time in January.


1: The @at-root is necessary since Sass can't reliably figure out whether & was used in the selector like it can when & is used without #{}.

@acdlite
acdlite commented Dec 20, 2013

This makes lots of sense. Thanks for so honestly and coherently explaining the change!

@chriseppstein

@nex3 Just allowing &- is not sufficient. We should allow & followed by any number of dashes or underscores.

@nex3 I'm already relying on & == null when no selector is in scope in compass. Can we add a helper function to detect whether & is even legal to use for 3.3?

@dahfazz
dahfazz commented Dec 20, 2013

This is your xmas gift. We are not going to spend xmas holydays coding with SASS 3.3, and spend time with family.

See you in January ^^

@jensgro
jensgro commented Dec 20, 2013

You leave me puzzled. You cannot mean, that you ban & from Sass?! This would mean I could not write:

a {
  color: red;
  &:hover, &:focus {
      color: blue;
  }
}

I definitely must have a problem in understanding, as English ain't my native language. Please say that I don't understand you correctly.

@chriseppstein

@jensgro No, everything that worked in Sass 3.2 will continue to work as you expect. We're saying that a change that we introduced for 3.3 will be partially removed. This change was to allow & to be used in script expressions instead of just in selectors.

@nex3
Owner
nex3 commented Dec 20, 2013

@chriseppstein

Just allowing &- is not sufficient. We should allow & followed by any number of dashes or underscores.

Yes; I thought this was implicit. To clarify, what's now allowed is & followed by any identifier. & followed by a number will also work.

I'm already relying on & == null when no selector is in scope in compass. Can we add a helper function to detect whether & is even legal to use for 3.3?

I'd rather delay this to 3.4 as well, since adding a function for it when we know it won't be necessary in the near future seems bad. Can you live with another release of erroring out when there's no parent?

@franks921

First off, thanks a lot for all the work you guys put into making Sass as awesome as it is.

So if I am understanding all this correctly, does that mean that mixins like this will no longer work in Sass 3.3?

@pdaoust
pdaoust commented Jan 8, 2014

@fr4nktic: yes, that's correct; that mixin will no longer work. The good news, if I understand this bit of news, is that the following will work:

=e($name)
  @at-root &__#{$name}
    @content
=m($name)
  @at-root &--#{$name}
    @content
@glebm
glebm commented Feb 10, 2014

Can someone clarify whether it will be possible to do this in sass 3.3:

=text-emphasis-variant($color) 
  color: $color
  a&:hover
    color: darken($color, 10%)

.text-success 
  +text-emphasis-variant($state-success-text)

In 3.2 we are doing this as a workaround:

=text-emphasis-variant($parent, $color)
  #{$parent} 
    color: $color
  a#{$parent}:hover
    color: darken($color, 10%)

@include text-emphasis-variant('.text-success', $state-success-text)
@franks921

@pdaoust Thanks for clarifying, I suspected as much. That works great if you don't have to @extend the parent, like this 3.3.0.rc.2 mixin does.

=m($name)
  @at-root
    #{&}--#{$name}
      @extend #{&}
      @content

I tried @extend &, but that is not allowed anymore. I guess I will have to resort to passing the parent as an argument to that mixin in 3.3.0.

=m($name, $parent)
  &--#{$name}
    @extend #{$parent}
    @content
@franks921

After trying a few more things, it seems my bem mixin will not work due to the limitations of extendeding &. Unless anyone else has any better ideas, this is what I will be doing to retain this functionality during 3.3 and to make sure it is still viable to use in 3.4 by making the $extend argument optional.

@panec
panec commented Mar 13, 2014

In 3.3.0 RC 2 I had a function:

@mixin inject-parent($selector, $depth: 0) {
    @if $depth > 0 {
        @at-root {
            // '&' was a double-wrapped list
            $path: nth(&, 1);
            // insert-nth($list, $index, $value)
            #{insert-nth($path, length($path) - $depth + 1, unquote($selector))} { @content; }
        }
    }
    @else {
        &#{$selector} { @content; }
    }
}

Usage of it was extremely simple:

.test {
    .test2 {
         @include inject-parent(" .test3", 1) {
             color: #FFF;
         }
    }
}

Because the & was a list of selectors ((".test" ".test2")) script worked and that was generating following css:

.test .test3 .test2 {
    color: #FFF;
}

I know that the full availability for & for sass scripts will not be a part of 3.3 but can the part when it will be a list of css selectors achieved, even via external Ruby script that will be added as custom plugin? It will allow a lot of developers to write workarounds before full support will be available.

@panec
panec commented Mar 17, 2014

For those that still need a list of parents as a list of strings, you can use -r (pass custom Ruby script) option when building sass files, with following content:

module Sass::Script::Functions
    def parentSelector()

        def opts(value)
            value.options = options
            value
        end

        selector = environment.selector
        return opts(Sass::Script::Value::Null.new) unless selector
        opts(Sass::Script::Value::List.new(selector.members.map do |seq|
            Sass::Script::Value::List.new(seq.members.map do |component|
                Sass::Script::Value::String.new(component.to_s)
            end, :space)
        end, :comma))

    end
    declare :parentSelector, []
end

and modify mixin:

@mixin inject-parent($selector, $depth: 0) {
    @if $depth > 0 {
        @at-root {
            // '&' was a double-wrapped list
            $path: nth(parentSelector(), 1);
            // insert-nth($list, $index, $value)
            #{insert-nth($path, length($path) - $depth + 1, unquote($selector))} { @content; }
        }
    }
    @else {
        &#{$selector} { @content; }
    }
}

Works like a charm in Sass 3.3.3

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