What Nobody Told You About Sass’s @extend

Kitty Giraudel
Share

Sass provides a lot of powerful features to write consistent and robust CSS. One of the most powerful yet tricky ones has to be @extend. While most Sass users understand the basics of @extend, I feel there are some more obscure parts to it that are not as well known.

But first, let’s start with the basics.

The Basics of @extend

The Sass @extend directive provides a simple way to allow a selector to inherit the styles of another one. This is especially useful in component-based CSS architecture, allowing us to easily make variations from a component by extending it.

Many of you might already know how to extend a selector but for those who don’t, here is the most basic case of selector extension in Sass:

.message {
  padding: .5em;
}

.message-error {
  @extend .message;
}

Resulting in the following CSS:

.message, .message-error { 
  padding: .5em;
}

Easy, right? Of course you’re not limited to extending classes; you can extend tag selectors (e.g. a), IDs (e.g. #id) — basically any valid CSS selector. And you can extend Sass placeholders (e.g. %placeholder) which are explicitly made for this.

Note: In a previous article, Sass: Mixin or Placeholder?, I covered the topic of placeholder in more detail. You can also check out placeholder on the official Sass docs.

Extending a deeper selector

In most cases you will extend very simple selectors (mostly classes), but nothing prevents you from extending a more complex selector like .class element:pseudo or even:

.message + .message {
  margin-bottom: .5em;
}

.message-error {
  @extend .message;
}

Which would output:

.message + .message, 
.message-error + .message-error, 
.message + .message-error, 
.message-error + .message { 
  margin-bottom: .5em;
}

The result is now a little more complex, but if you understand @extend, the output shouldn’t surprise you too much.

Multiple extends

You might be aware that you can extend multiple selectors from within the same rule but did you know you can do so with a single @extend directive?

.message {
  padding: .5em;
}

.important {
  font-weight: bold;
}

.message-error {
  @extend .message, .important;
}

Output:

.message, .message-error {
  padding: .5em;
}

.important, .message-error {
  font-weight: bold;
}

If you ask me, I don’t prefer this method. While it makes the code a bit shorter, I find it it’s harder to read. Using distinct directives for each extended selector makes it easier for the eye to spot the selectors and how many are being extended.

You could work around the syntax like this:

.message-error {
  @extend
    .message,
    .important;
}

But how much more readable is that?

I will continue to use one @extend directive per line, but the result will be the same so feel free to pick the syntax you’re most comfortable with.

Chaining extends

As just discussed, a selector can @extend from multiple sources. But you can also chain your @extend directives:

.message {
  padding: .5em;
}

.message-important {
  @extend .message;
  font-weight: bold;
}

.message-error {
  @extend .message-important;
}

And the output:

.message, .message-important, .message-error {
  padding: .5em;
}

.message-important, message-error {
  font-weight: bold;
}

Although the above example is valid, I would advise against doing this since it can have undesired effects. Let’s discuss those right now.

Massive extending

Probably one the main arguments people make against CSS preprocessors is: “Look at the output, it’s horrifying!” In a way, they are right. The Sass @extend directive is so powerful it can result in unusually large extends.

This is why you should always be careful when extending a selector because it will extend all the occurrences of the selector — which can quickly get messy. Consider the following example:

.important {
  font-weight: bold;
}

.sidebar .important {
  color: red;
}

.message {
  @extend .important;
}

This will output:

.important, .message {
  font-weight: bold;
}

.sidebar .important, .sidebar .message {
  color: red;
}

As you can see, not only has .message inherited from .important but .message has also inherited from the instances where .important is used in a descendant selector too. While this can be what you’re expecting, this is not always the case, so you should use the @extend directive carefully. Either make sure the selector you’re extending from only exists in one place in your CSS, or extend a placeholder — that’s what they’re for.

So keep in mind that Sass doesn’t create bad code, bad coders do.

Preserving source order

One very little known characteristic of @extend in Sass is the way it deals with source order. Let’s have a look at the following code snippet:

.half-red {
  color: rgba(red, .5);
}

.message-error {
  color: red;
  @extend .half-red;
}

Unsemantic class name aside, this is perfectly fine code, right? You would probably expect something like the following when it compiles:

/* This will not be the correct output! */
.message-error {
  color: red;
  color: rgba(255, 0, 0, 0.5);
}

This looks like a simple way to provide graceful degradation for older browsers that don’t support rgba() color values (we won’t expand on the reasons behind using an @extend here; it doesn’t matter). But this is not what the CSS output will look like. Au contraire, it will look like this:

.half-red, .message-error {
  color: rgba(255, 0, 0, 0.5);
}

.message-error {
  color: red;
}

But why?! you probably say unhappily. This is because the @extend directive works in the reverse way that you would expect. From the Sass documentation:

@extend works by inserting the extending selector […] anywhere in the stylesheet that the extended selector […] appears.

In our example, the “extending selector” is .message-error and the “extended selector” is .half-red.

Optional extends

Depending on the kind of application you’re working on, you may or may not be using third-party frameworks or CSS-based widgets. If some of these are written in Sass, there is nothing preventing you from extending selectors from those files.

Unfortunately if the extended selector happens to be missing, Sass will throw an error and the compilation will fail.

“.message-error” failed to @extend “.important”.
The selector “.important” was not found.
Use “@extend .important !optional” if the extend should be able to fail.

As you can see, you can pass an !optional flag to your imports to prevent them from failing if ever the extended selector is not found. This is useful also in the case where the extending and the extended selectors are conflicting:

a.important {
  font-weight: bold;
}

p.message-error {
  @extend .important;
}

Indeed, this throws an error because both selectors are specified, making them impossible to unify. Thus you’ll get the following error:

No selectors matching “.important” could be unified with “p.message-error”.

Adding the !optional flag to your @extend would resolve this issue.

Extending and media queries

One of the biggest issues with @extend is its lack of support for extending from within a @media directive. Unfortunately, Sass doesn’t allow cross-media extensions:

.important {
  font-weight: bold;
}

@media (max-width: 767px) {
  .message-error {
    @extend .important;
  }
}

This will result in the following error:

You may not @extend an outer selector from within @media.
You may only @extend selectors within the same directive.

This is because @extend is basically about moving selectors around and not CSS rules as we saw in the section on preserving source order. If Sass allowed this, then extending a selector that is inside of another media query would produce something like this:

.rule, @media(max-width: 767px) { .another-rule }, .another-another-rule {
  /* ... */
}

… which is obviously not valid CSS.

That being said, Sass developers are well aware of this issue (demonstrated by the outrageous number of issues mentioning this on their repo: #501, #640, #915, #1050, #1083).

Since this is a major downside to advanced Sass architecture, they are likely to implement a solution to this problem soon. According to this comment from Nex3 (Sass lead developer), it will be mixin interpolation.

Best Practices for @extend

To sum up, here are what I would call best practices when using the @extend directive in Sass:

  • Make sure the extended selector is present only once in the stylesheet.
  • Avoid extending from nested selectors.
  • Avoid chaining @extend directives.
  • Don’t try extending from within a media query; it doesn’t work.

That wraps up this discussion of @extend. If you have anything to add, let me know in the comments!

This article was translated into French by Pierre Choffé for La Cascade