Everything you need to know about Sass 3.4

Micah Godbolt, Developer
Posted

Sass 3.4 may not be as groundbreaking as 3.3 was, but it does comes with one fundamental shift that deserves some careful attention. This feature actually made a brief appearance in Sass 3.3 betas, but was eventually pulled for further refinement. By the looks of the thing, it was well worth the wait!

In Sass 3.4 we will finally have access to the strings of selectors in the parent selector, or "&".

  1. ul {
  2. li {
  3. background: blue;
  4. &:hover {
  5. background: red;
  6. }
  7. }
  8. }

Anyone familiar with Sass will probably recognize what I am doing in the above block of code. The list item has a blue background until you hover over it, wherein it turns red. The magic here is in the "&" character. Instead of writing li:hover  directly below the li  I am able to nest my code inside of the tag. The & represents ul li , so the resulting code is ul li:hover .

Now, while the '&' character represents a string of selectors, we have never been able to access or edit that string. We could never set '&' to a variable or perform string functions like nth()  or join()  on it. Sass 3.4 finally gives us those abilities, and brings a bevy of other useful functions as well. Let's dive into some examples.

  1. nav{
  2. ul {
  3. li {
  4. $selector: &;
  5. foo: $selector; // nav ul li
  6. bar: length($selector); // 1
  7. }
  8. }
  9. }

First thing you'll notice is that in 3.4 we can set $selector: &  without any problem. You don't have to set '&' to a variable to use it, though. You can perform list functions directly on it.

Next we perform a series of basic list functions on it. Printing out $selector we see that it is set to nav ul li . Performing length($selector)  you might expect a value of "3" instead of "1", so you might be scratching your head until you realize that these are lists of lists. Let's look at the next example and this will be clearer.

  1. nav, header .container{
  2. ul {
  3. li {
  4. $selector: &;
  5. foo: $selector; // nav ul li, header .container ul li
  6. bar: length($selector); // 2
  7. baz: length(nth($selector, 2)); // 4
  8. qux: nth(nth($selector, 2), 1); // 'header'
  9. }
  10. }
  11. }

Here you can see after "foo" that we have a comma delimited list of 2 different space delimited lists. This means our length function will properly return "2". If we use nth()  function to select just one of those lists, we can properly test the number of items it has (4). We can even double up our nth()  functions and even get at the actual value of the selector ("header").

There's a function for that

Let's look at a quick contrived example:

  1. nav{
  2. ul {
  3. li {
  4. $new-selector: append(nth(&, 1), a);
  5. @at-root #{$new-selector} {
  6. color: pink;
  7. }
  8. }
  9. }
  10. }
  11.  
  12. // Resulting CSS
  13.  
  14. nav ul li a {
  15. color: pink;
  16. }

Here I am using append to add an extra selector to nav ul li . It works! You get nav ul li a , but it's pretty ugly, and Sass 3.4 gives us functions to handle this in a much cleaner fashion. Let's look at a couple of simple functions, and the css that they replicate.

First is selector-nest($selectors...)

  1. #{selector-nest(".foo, .bar", ".qux")} {
  2. background-color: red;
  3. }
  4.  
  5. .foo, .bar {
  6. .qux {
  7. background-color: red;
  8. }
  9. }
  10.  
  11. // Both result in
  12.  
  13. .foo .qux, .bar .qux {
  14. background-color: red;
  15. }

 

Next is selector-append($selectors...)

  1. #{selector-append(".foo .bar", ":hover")} {
  2. background: pink;
  3. }
  4.  
  5. .foo .bar{
  6. &:hover {
  7. background: red;
  8. }
  9. }
  10.  
  11. // Both result in
  12.  
  13. .foo .bar:hover {
  14. background: red;
  15. }

You can see how these two functions allow us to replicate the nesting and appending behavior we were already able to do in our Sass partials. So while we don't get the ability to do anything "new", we are able to create and modify selectors in a way that is more programatic, and less prone to errors.

Where the real magic happens

One of my biggest pet peeves of Sass has been that while it was really good at helping with local context (&:hover) and global context (.lt-ie9 &), it provided absolutely no way of setting component level contexts. What does that mean? Well, let me show a simple example I just ran into this week.

  1. .tabs {
  2. .tab {
  3. background: red;
  4. &:hover {
  5. background: white;
  6. .tab-link {
  7. color: red;
  8. }
  9. }
  10. .tab-link {
  11. color: white;
  12. }
  13. }
  14. }

In my Sass partials I try to write each selector in only one single location. In this example you can see that I failed because I wrote .tab-link  twice. It's no huge loss here, but as this partial gets larger, and more selectors get repeated, it can be difficult to keep track of what happens to .tab-link  in every context.

In Sass 3.3 we don't have a better solution for this. We need .tabs .tab:hover .tab-link but we can't insert a context "inside" of the selector. We can only append, or prepend. In Sass 3.4 we finally have the fix!

  1. .tabs {
  2. .tab {
  3. background: red;
  4. &:hover {
  5. background: white;
  6. }
  7. .tab-link {
  8. color: white;
  9. @at-root #{selector-replace(&, '.tab', '.tab:hover')} {
  10. color: red;
  11. }
  12. }
  13. }
  14. }

selector-replace($selector, $original, $replacement)

  is a powerful function that packs several list functions into one. First it breaks the compound selector into individual selectors. It then matches the selector you passed into $original .tab and replaces it with the $replacement .tab:hover before zipping it all back together again.

When I first imagined the ability to manipulate the strings in "&", I expected we'd have to do this process manually. So I'm quite grateful this function comes built in!

But as powerful as this function is, it's still a bit clunky to use in every day code. The @at-root , extrapolation and the inclusion of '&' as a parameter seem a bit unnecessary. Let' see what this function would look like as a nice mixin.

  1. @mixin context($old-context, $new-context) {
  2. @at-root #{selector-replace(&, $old-context, $new-context)} {
  3. @content;
  4. }
  5. }
  6.  
  7. .tabs {
  8. .tab {
  9. background: red;
  10. &:hover {
  11. background: white;
  12. }
  13. .tab-link {
  14. color: white;
  15. @include context('.tab', '.tab:hover') {
  16. color: red;
  17. }
  18. }
  19. }
  20. }
  21.  
  22. // Output
  23. .tabs .tab { background: red; }
  24. .tabs .tab:hover { background: white; }
  25. .tabs .tab .tab-link { color: white; }
  26. .tabs .tab:hover .tab-link { color: red; }

That's much cleaner and easier to read!

Sass 3.4 also includes the selector-extend($selector, $extendee, $extender) function, which behaves exactly like selector-replace() except that it returns a list of both the original selector as well as the "replaced" one. You'd probably use this more in frameworks, and less inline in your Sass.

Lastly selector-unify($selector1, $selector2)  and is-superselector($super, $sub)  perform some powerful comparison functions, and simple-selectors($selector)  helps you break down complex selectors. You can read all about them on the Sass change log.

Wrapping it up

While these changes are not quite as mind blowing as Maps, Sourcemaps and @at-root, Sass 3.4 brings some long wished-for functionality. Personally, I'm really looking forward to cleaner Sass partials, and taking control of those component level contexts that tripped me up so many times before. If you want to see a few more examples, make sure to check out the @SassBites video tutorial on the selector-replace() function.

Micah Godbolt

Developer