Understanding placeholder selectors
Sass provides a number of different ways to share code between CSS rules. You can use mixins to insert new CSS properties and/or rules into your CSS and you can use @extend
to share CSS properties between selectors. Sass 3.2 introduces a new concept called "placeholders" to make @extend
generate more efficient output.
But before we get into that, let's talk about how extend works…
How extend works
The @extend
directive allows us to easily share styles between selectors. This is best illustrated with an example:
.icon {
transition: background-color ease .2s;
margin: 0 .5em;
}
.error-icon {
@extend .icon;
/* error specific styles... */
}
.info-icon {
@extend .icon;
/* info specific styles... */
}
Which will generate the following output:
.icon, .error-icon, .info-icon {
transition: background-color ease .2s;
margin: 0 .5em;
}
.error-icon {
/* error specific styles... */
}
.info-icon {
/* info specific styles... */
}
What's going on here? The @extend
directive allows us to declare that .error-icon
and .info-icon
should inherit the properties of the .icon
selector. It does this by modifying the .icon
selector to also include .error-icon
and .info-icon
. Pretty nifty, right?
Now here comes the interesting part. What if we never use the icon
class in our markup and its only purpose is to be there to extend? The resulting CSS will be slightly larger than it really needs to be because we'll have a style that will never be used. We can get around this with placeholder selectors.
Enter placeholder selectors
Placeholder selectors were introduced to solve this exact problem. They are very similar to class selectors, but instead of using a period (.
) at the start, the percent character (%
) is used. Placeholder selectors have the additional property that they will not show up in the generated CSS, only the selectors that extend them will be included in the output.
Going back to our initial example, if our icon styles are defined like so:
%icon {
transition: background-color ease .2s;
margin: 0 .5em;
}
.error-icon {
@extend %icon;
/* error specific styles... */
}
.info-icon {
@extend %icon;
/* info specific styles... */
}
The following CSS will be generated:
.error-icon, .info-icon {
transition: background-color ease .2s;
margin: 0 .5em;
}
.error-icon {
/* error specific styles... */
}
.info-icon {
/* info specific styles... */
}
Notice how .icon
is no longer present in the compiled CSS!
Extend vs. include
At first glance it may look like placeholder selectors are the same as parameterless mixins. While this is almost true from a functional perspective (it will achieve nearly identical results in the browser), the CSS that is generated differs drastically.
Consider the implementation of the icon example using mixins:
@mixin icon {
transition: background-color ease .2s;
margin: 0 .5em;
}
.error-icon {
@include icon;
/* error specific styles... */
}
.info-icon {
@include icon;
/* info specific styles... */
}
This will generate the following CSS:
.error-icon {
transition: background-color ease .2s;
margin: 0 .5em;
/* error specific styles... */
}
.info-icon {
transition: background-color ease .2s;
margin: 0 .5em;
/* info specific styles... */
}
From a maintenance perspective this is just as good as the @extend
example, but if you are concerned about the CSS output, this is much worse because the properties are duplicated between rules instead of sharing the same selector.
Limitations
One limitation with @extend
that applies to placeholder selectors as well is that it doesn't work between rules in different @media
blocks.
Consider the following:
%icon {
transition: background-color ease .2s;
margin: 0 .5em;
}
@media screen {
.error-icon {
@extend %icon;
}
.info-icon {
@extend %icon;
}
}
This will actually result in a compile error:
You may not @extend an outer selector from within @media. You may only @extend selectors within the same directive. From "@extend %icon" on line 8 of icons.scss
When I first ran into this limitation I thought it was a bug. However, there is a very good reason for why it works this way in Sass.
Since @extend
works by adding a selector to another selector without duplicating any of the properties it's actually impossible to join selectors in different @media
blocks.
It does work the other way though. Any media queries surrounding the placeholder selector will be applied to the selectors extending it providing they are not in an @media
block:
@media screen {
%icon {
transition: background-color ease .2s;
margin: 0 .5em;
}
}
.error-icon {
@extend %icon;
}
.info-icon {
@extend %icon;
}
This will compile to:
@media screen {
.error-icon, .info-icon {
transition: background-color ease .2s;
margin: 0 .5em;
}
}
Final words
The @extend
and @include
directives are both very powerful features with some subtle differences. When approaching a style reuse problem you may want to ask yourself if the generated CSS is important to you. In some cases @extend
can greatly simplify the output and significantly improve the performance of your CSS.