Why You Should Avoid Sass @extend

About a year ago, I wrote Mixin or Placeholder (my first article here at SitePoint) immediately followed by What Nobody Told You About Sass Extend. And here I am, one year later, changing my mind again and writing why I think the @extend directive from Sass is really far from being the Eldorado.

TL;DR: Extending is invisible. Extending doesn’t necessarily help file weight, contrary to the saying. Extending doesn’t work across media queries. Extending is not flexible. Mixins have absolutely no drawback.

But first, I should probably give you a little reminder. @extend is a Sass feature aiming at providing a way for a selector A to extend the styles from a selector B. When doing so, the selector A will be added to selector B so they both share the same declarations. For instance:

.selector-A {
    @extend .selector-B;
    unicorn: true;
}

.selector-B {
    rainbow: true;
}

This Sass snippet will yield this CSS:

.selector-A {
    unicorn: true;
}

.selector-B,
.selector-A {
    rainbow: true;
}

That is pretty much all you need to know about @extend to fully appreciate this article so now that we’ve covered the basics, it’s time to move on. There are quite a few reasons why I think @extend is a deceptive feature.

However worry not, this is not yet another article about how "Sass @extend can output outrageously long selectors if you’re messing with it". Let’s try to dig deeper than that, shall we?

Extending is invisible

Sass is just a tool. It is meant to help developers write CSS. However in this case write means so much more than just writing code. It also means maintain. Sass is a tool to help people come back to a stylesheet and update the code without spending hours figuring what’s going on and where else this #FA7A55 color lies.

Because of this, Sass as any other tool, should be transparent. When writing a line of Sass, we should be able to figure out what CSS would result from it. For instance, if I give you this:

// _mixins.scss
@mixin center($max-width) {
    max-width: $max-width;
    width: 100%;
    margin: 0 auto;
}

// main.scss
.container {
    @include center(1170px);
}

You can tell me just by looking at the mixin that the resulting CSS will be:

.container {
    max-width: 1170px;
    width: 100%;
    margin: 0 auto;
}

It is very easy to figure this out because you know that including a mixin takes the content of the mixin, replaces the variables with the given arguments and prints it at the very position the mixin has been included. This is actually what’s cool with mixins, they are obvious.

And this is the first problem with @extend. Whenever you decide to extend a selector, you have absolutely no idea what is going to happen. For what you know, the result could range from doing nothing to causing disastrous side-effects. The extended selector could be present once (best scenario), or multiple times inside compounded selectors which would be terrible.

Now, there are obviously cases where you know that the extended selector is present once and only once in the whole stylesheet, in which case it’s perfectly fine to extend it. That’s the case for helpers for instance. Consider:

%clearfix::after {
    content: '';
    display: table;
    clear: both;
}

This is usually part of your _helpers.scss file, included very close to the top of your stylesheet. The %clearfix placeholder won’t be part of any other selector. Ever. Thus, extending it is quite transparent: the extending selector will jump on top of the stylesheet to join %clearfix::after.

Anyway, this leads me to think that there is a single valid reason to extend a selector: it is a helper that exists only once in the whole stylesheet. And even for this… I think a mixin is still better.

Extending is not necessarily better than mixins regarding performance

There is an old rumor stating that @extend is better than mixins for performance reasons because they group selectors rather than duplicating the same CSS declarations over and over again. It’s common to say that:

%toolicorn {
    unicorn: rainbow;
    status: happiness;

    &::grandeur {
        level: infinite;
    }
}

.wanna-be-a-unicorn {
    color: hotpink;
    @extend %toolicorn;
}

.wanna-be-a-unicorn--too {
    color: deepskyblue;
    @extend %toolicorn;
}

… is much better for performance than:

@mixin toolicorn {
    unicorn: rainbow;
    status: happiness;

    &::grandeur {
        level: infinite;
    }
}

.wanna-be-a-unicorn {
    color: hotpink;
    @include toolicorn;
}

.wanna-be-a-unicorn--too {
    color: deepskyblue;
    @include toolicorn;
}

On paper, that’s very true. The first CSS output is both shorter and possibly more elegant than the second. See for yourself, this is the @extend version (285 characters):

.wanna-be-a-unicorn,
.wanna-be-a-unicorn--too {
    unicorn: rainbow;
    status: happiness;
}

.wanna-be-a-unicorn::grandeur,
.wanna-be-a-unicorn--too::grandeur {
    level: infinite;
}

.wanna-be-a-unicorn {
    color: hotpink;
}

.wanna-be-a-unicorn--too {
    color: deepskyblue;
}

And this is the mixin-powered version with 304 characters:

.wanna-be-a-unicorn {
    color: hotpink;
    unicorn: rainbow;
    status: happiness;
}

.wanna-be-a-unicorn::grandeur {
    level: infinite;
}

.wanna-be-a-unicorn--too {
    color: deepskyblue;
    unicorn: rainbow;
    status: happiness;
}

.wanna-be-a-unicorn--too::grandeur {
    level: infinite;
}

For such a small example, the difference is quite minimal however it can quickly ramp up with larger content for the mixin/placeholder. So again, on paper it makes sense to think that extending placeholders rather than including mixins is better for file size.

However files are usually served gzipped from the server. Gzip is based on DEFLATE and LZ77; quickly put, the more a string is repeated, the better it is for compression. When putting this in context, it means that the difference is likely to be inexistant once Gzip has rolled over the whole thing.

This is exactly the same debate as for duplicated media queries when putting them inside selectors. It makes no difference once Gzip has done its thing:

… we hashed out whether there were performance implications of combining vs scattering Media Queries and came to the conclusion that the difference, while ugly, is minimal at worst, essentially non-existent at best.
Sam Richards in regards to Breakpoint

So, using @extend rather than mixins because of code duplication is not a valid point. Moving on.

Extending is not possible across media contexts

You know about that, right? When trying to extend an outer selector from within a @media directive, Sass chokes and says:

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

There have been attempts to polyfill this with hack-y solutions (including mine published here on SitePoint), but this is not a good idea. You’d be a fool to use such things in production.

Reasons why we cannot extend across different media contexts are strictly technical at this point. Still, this is something quite annoying to deal with. To work around this issue, the less worse idea is to wrap placeholders with a mixin so you can choose either to @extend or to include, depending on whether your in a @media block or not. I described this technique here and this is still what I use at work. Quick proof of concept:

// _helpers.scss
@mixin clearfix($extend: true) {
    @if $extend == true {
        @extend %clearfix;
    } @else {
        &::after {
            content: '';
            display: table;
            clear: both;
        }
    }
}

%clearfix {
    @include clearfix($extend: false);
}

// main.scss
.container {
    @include clearfix;
}

@media (min-width: 42em) {
    .my-other-selector {
        @include clearfix($extend: false);
    }
}

It works fine and does not involve much complication as far as I know, yet it’s still another layer of abstraction in some way. However, the code might looks a bit complex for someone getting started with Sass.

Anyway, you cannot extend a selector across different media directives, and this plain sucks. For this reason, it might be worth moving to a mixin based setup, where styles might get repeated (see section Extending is not necessarily better than mixins regarding performance) but you can do whatever you want, whenever you want, the way you want.

Extending is not flexible

By their own definition, mixins make a more powerful feature than @extend. Indeed, they:

All those reasons make them the feature of choice if you are heading towards flexibility. It gets very easy to add an extra argument to a mixin because you need it to do more, without introducing any API break whatsoever.

On the other hand, @extend is quite strict. If you need your code snippet to accept a variable, you have to change the placeholder into a mixin, then update all the rules extending this placeholder to include the mixin instead. Not only quite tedious, but certainly not very flexible.

Final thoughts

So @extend sucks, is that it? I am not sure. Maybe I am simply not skilled enough to see the real potential of this feature, but it seems I am not the only one to realize @extend is really not that a killer-feature. Indeed, I suggest you read those articles to push things further:

Anyway. @extend can be messy. My advice would be to avoid it when possible and use mixins instead, because at the end of the day, it makes absolutely no difference.

Replies

  1. Very eye-opening article, Hugo, but here's a good potential use-case for placeholders and @extend: http://sassmeister.com/gist/08aee2e0b393924cec84

    Basically, the combination of placeholders and @extend can be used to flexibly establish relationships between abstract components, especially when the selectors used to represent these components can change without ever having to change the underlying CSS.

  2. Nice article. I've already thought about switching to includes in my future projects, as my co-worker has trouble reading the long selectors in the dev-tools (i.e. clearfix, which is extended 10 times).

    One big advantage of @extend is, however, that you may reuse pure CSS components from external projects without the need of modifying them. As I don't wanna have some hybrid approach, where i sometimes use @extend and sometimes @include to extend - would it make sense to create an extend mixin like this?

    http://sassmeister.com/gist/dbe1cb5a682f3d695395

    1. Nope, you do not suffer from any performance drawback with lists of selectors. The performance bottleneck is having to find a selector deeply nested for the parser because it goes from right to left. So for instance .a .b .c would mean finding all .c elements, then filtering them to keep only the one in a .b element. Then filter them to keep only those within a .a element, and so on. You don't have to do this with lists of selectors since they are completely unrelated.

    2. You're obviously doing things wrong then. wink

  3. Thanks @HugoGiraudel, that's really insightful. Unfortunately, as a freelancer, I do not always have a say in the server setup so gzip is not always possible, hence my question. It strikes me, then, that very cautious/careful use of @extend could still be beneficial in these less-than-ideal instances?

  4. Gzip (or MOD_DEFLATE) is probably the biggest change you can make to a site to boost performance. I highly suggest you try using Gzip at all cost before considering optimising your CSS.

8 more replies