Skip to content

Instantly share code, notes, and snippets.

@nex3
Last active January 18, 2024 00:24
Show Gist options
  • Save nex3/8050187 to your computer and use it in GitHub Desktop.
Save nex3/8050187 to your computer and use it in GitHub Desktop.

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 #{}.

@nex3
Copy link
Author

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?

@frank-who
Copy link

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
Copy link

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
Copy link

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)

@frank-who
Copy link

@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

@frank-who
Copy link

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
Copy link

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
Copy link

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