Where extended placeholder selectors go wrong in libsass

When working between the Ruby version of Sass and libsass, we are constantly reminded that these are two very separate implementations of a specification. Things are bound to go wrong. And here is a very real example.

The github thread

If you are interested in following along and/or are able to assist with this difficult functionality that is so desperately needed in the community, please follow this thread.

What goes wrong?

When writing an placeholder selector in Ruby Sass, you could so the following code example. Here I created a parent selector, but using the % syntax the whole selector never appears in the output CSS until the placeholder selector is extended with a standard selector.

%new-parent {
  border-width: 1px;
  border-style: solid;
  .background-color {
    background-color: orange;
  }
  .add-border {
    border: 1px solid red;
  }
}

Using the placeholder selector with a standard selector:

.outer-box {
  @extend %new-parent;
}

we get the following processed CSS with Ruby Sass:

.outer-box {
  border-width: 1px;
  border-style: solid;
}
.outer-box .background-color {
  background-color: orange;
}
.outer-box .add-border {
  border: 1px solid red;
}

Using the same Sass example from above, but using the libsass library to process the CSS, I get the following:

.outer-box {
  border-width: 1px;
  border-style: solid; }
  %new-parent .background-color {
    background-color: orange; }
  %new-parent .add-border {
    border: 1px solid red; }

Without digging too far into details, what we see happening here is that the extension of the parent selector is not being carried forward into the child selectors and simply outputting the silent selector's name. And of course, this is not valid CSS and will render nothing.

Not is all lost!

This is not to say that extends are totally broken in libsass. If I were to create another complex selector with nested selectors, instead this time all the selectors are placeholders, there is no issue with the output.

Notice in the following how all selectors are designated with the % symbol which renders them invisible from processing.

%default-parent {
  border-width: 1px;
  border-style: solid;
  %set-background-color {
    background-color: orange;
  }
  %set-border {
    border: 1px solid red;
  }
}

To allow for all the placeholder selectors to be processed into CSS, I simply need to extend them to a standard selector as illustrated in the following example:

.parent {
  @extend %default-parent;
  .block {
    @extend %set-background-color;
    @extend %set-border;
  }
  .another-block {
    @extend %set-border;
  }
}

This technique will produce the following CSS:

.parent {
  border-width: 1px;
  border-style: solid; }
  .parent .block {
    background-color: orange; }
  .parent .block, .parent .another-block {
    border: 1px solid red; }

This is actually ok

Really, when you think about it, this is still very useful. The technique of rendering pre-written CSS selectors within a more complex parent selector is an easy way to reproduce UI code, but are you really reusing properties correctly? If you need to reuse a complex selector's properties, but a child selector has a different name for one reason or another, this will break.

The technique of making every child selector within a parent selector a placeholder allows for greater flexibility in application. Not only with being able to change the name of the standard selector which will be extended to, but the ability to NOT extend a CSS rule.

In the following example, I am calling the %default-parent placeholder selector, but not extending the nested %set-border placeholders. The following output represents this decision clearly.

.parent {
  border-width: 1px;
  border-style: solid; }
  .parent .block {
    background-color: orange; }

There is one more issue

I recently discovered that even going with the 'all placeholder selector' model, there is one more issue, pseudo elements. In the following example notice how all the selectors within are placeholders and within %tab-ui-pattern is &:last-child.

%nav-ui-pattern {
  width: 100%;
  border-bottom: 1px solid black;
  margin-bottom: em(20);
  %tab-ui-pattern {
    display: inline-block;
    padding: em(20) em(40);
    margin-right: em(20);
    border: {
      width: 1px 1px 0 1px;
      style: solid;
      color: black;
      radius: em(5) em(5) 0 0;
    }
    &:last-child {
      margin: 0;
    }
  }
}

If I were to use this pattern in the following way:

.primary-header-navigation {
  @extend %nav-ui-pattern;
  a {
    @extend %tab-ui-pattern;
  }
}

Currently in libsass, I will get the following output. Notice were we see %nav-ui-pattern %tab-ui-pattern:last-child, but are expecting .primary-header-navigation a:last-child.

.primary-header-navigation {
  width: 100%;
  border-bottom: 1px solid black;
  margin-bottom: 1.25em; }
  .primary-header-navigation a {
    display: inline-block;
    padding: 1.25em 2.5em;
    margin-right: 1.25em;
    border-width: 1px 1px 0 1px;
    border-style: solid;
    border-color: black;
    border-radius: 0.3125em 0.3125em 0 0; }
    %nav-ui-pattern %tab-ui-pattern:last-child {
      margin: 0; }

This last part is addressable using Mixins, but that is far from ideal.

In closing

So in the end, extends in libsass perform much as you would expect them to with single selectors. I am not saying that it's implementation is perfect, the libsass team has stated that this needs to be addressed. But now at least you have an example where this will fail and how it will fail.

Check it out for yourself at SassMeister