HTML & CSS
Article

Why You Should Avoid Sass @extend

By Hugo Giraudel

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.

Meet the author
CSS goblin. Sass hacker. Margin psycho. Author of Sass Guidelines, SassDoc, Sass Compatibility, Browserhacks and a lot of Sass stuff.
Comments
DavidKPiano

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.

MattDiMu

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

MattDiMu

Great idea using @extend to handle relationships between parent and child abstract components. Theoretically you're right, but have you already used this in a real project?

In my projects I usually have
- some base css applying everywhere (normalize, default styling of forms/tables/links, etc. )
- some external css (from plugins like sliders, overlays, ...)
- a set of helper classes/placeholders (i.e. clearfix, visuallyhidden, ...), which i may extend/include (deciding to switch) wherever needed
- a grid / structure css, which defines the general structure / areas of my templates
- abstract components, which i may extend/include (deciding to switch as well)
- components

Up to now, I've never had the use case of using nested abstract components. Yes, some of the "abstract components" are used within others, but it was never necessary to ensure, that they only work within a certain parent class.

DavidKPiano

Thanks @MattDiMu - our development team is using this technique (and an architectural structure based on this) in the next version of our project, which is a large front-end application.

By considering that everything is a component, we have found that component nesting is inevitable, and that it's important to maintain parent-child component relationships in a way that's flexible and thoughtful of separation of concerns.

Another reason you may want to use %placeholders is to maintain coherency within your styles - something that is neglected as soon as you treat your components as isolated modules (which, unfortunately, seems to be the trend nowadays).

As a contrived example, let's say you have a %panel component that has a certain look and feel, and you want a %modal component to use the panel's style (for coherency), but also to add other styling, such as fixed positioning and an overlay. By using @extend with placeholders, you have the flexibility to:

  • Change the styling of the %panel and have it automatically be reflected in the %modal, which was your original intention, and...
  • Use any selector nomenclature you want for %panel and %modal, such as .foo-panel and .bar-modal, with the ability to change these without affecting the parallel relationship between the two.

Can you use mixins for this? Of course, but they will become more complex and unwieldy when you have more complex relationships between components. You'll have to add more arguments, and even if you decide to extend %panel within a @mixin modal(...), you remove the ability to create a %panel-slightly-modified component that will @extend %panel, should you ever want to have a %modal-slightly-different component that can @extend %panel-slightly-modified.

This is advanced usage, though, and we are fully cognizant of how @extend and %placeholders work, and use techniques to prevent them from causing selector explosion. For everyday usage, Hugo's article brings up some very good points, and if you aren't aware of the effects of extending placeholders, you should opt for a @mixin solution instead.

njessen

I agree. Very good use case. There are a lot of things I choose not to extend because it causes selectors to jump up the stylesheet and that is rarely a good thing. I'd rather only extend within a sass partial for things like buttons, panels, or other components. Then I know the selector will never jump up further in the output stylesheet than I intend.

MattDiMu

Thx for your reply. I see your point and have to admit, that I also tend to treat components as isolated modules wink

To keep coherency within my components, I use(d) to @extend multiple abstract placeholders, but i never tried to nest them. Might be useful sometimes, but also brings in more complexity.

iamkeir

It is good to read more articles about the gotchas with @extend. The use case for relationships in the comments is interesting too.

I do have two questions though, re 'Extending is not necessarily better than mixins regarding performance':

1) We have come to learn that .unnecessary .levels .of .class .specificity can be detrimental to performance - do lists/combined selectors suffer the same? (By which I mean the .really, .long, .class, .groupings, . caused, .by @extend)

2) What if you are not gzipping?

HugoGiraudel
  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

iamkeir

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?

HugoGiraudel

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.

iamkeir

Agreed but, as I said, not always within my power. Thanks for your article/comments.

MattDiMu

AFAIK, Browsers use CSS-Selectors slightly different: The Browser goes through its DOM-Elements and then tries to find matching selectors for this DOM-Element. As soon as one of the selectors doesn't fit (from right to left), the whole nested selector is skipped.

e.g. you have the following nested selector:

.a .b .c .d .e .f { property: value; }

and 100 DOM-Elements of which 4 have the class f and one of these four has parents with the classes a b c d and e. Then the Browser will
- 96 times only check the .f selector and skip it afterwards
- 4 times check the .e selector
- 1 time check the .d selector
- 1 time check the .c selector
- 1 time check the .b selector
- 1 time check the .a selector


104 checks are necessary

Even if you'd optimise your selector and reuduce it to

.d .e .f { property: value; }

it wouldn't make a significant difference, as still 101 checks would be necessary.

So even you've reduced the length of your selector from 6 to 3 individual selectors (-50%), you've reduced the amount of checks only by 2,8%.

Conclusio: Nested selectors are no performance bottleneck as long as your very right selector doesn't match too many elements (use classes).

Jakob_E

Gzip compression is used on 58.3% of websites* leaving 41.7% un-gzipped.
IMO this takes the gzip-will-fix-it argument of the table...
It would have been nice – but so would putting IE8+9 in the grave wink

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Front-end, once a week, for free.