By Harry Roberts
Harry Roberts is an independent consultant web performance engineer. He helps companies of all shapes and sizes find and fix site speed issues.
Written by Harry Roberts on CSS Wizardry.
This is a question I get asked a lot by my clients: When should we use a
mixin, and when should we use
@extend
?
There’s an old rule of thumb which states that mixins without arguments are
bad
—that mixins which just duplicate code with no difference between each
instance are nasty. The truth is that the answer is a lot more nuanced than that.
Let’s take a look.
@extend
Let me start by saying that I would generally advise never to use @extend
at
all. It is something of a Fool’s
Gold:
a feature with a lot of promise and twice as many caveats.
If you are definitely, completely set on using @extend
:
In theory, @extend
is great, but, in practice, there is just too much that can
go wrong. I have seen stylesheets more-than-double in size; I have seen source
order get destroyed; and I have seen clients plough right through their 4095
selector budget.
It is always better to err on the side of caution and omit any features or tools
that have the potential to cause so much trouble with little or no tangible gain.
Having to shard your stylesheets into less-than-4096-selector-groups as a result
of misusing a productivity tool is very, very counterintuitive.
N.B. I feel I should add that this isn’t me hating on @extend
per se;
there’s just a lot to be aware of and you must remain vigilant if you are going
to use it.
But, if you are going to use @extend
, when should you?
It is important to realise that @extend
creates relationships. Whenever
you use @extend
, you are transplanting a selector elsewhere in your stylesheet
in order for it to share traits with other selectors that are also being
transplanted. As a result, you are dictating that these selectors all share a
relationship, and misusing @extend
can create relationships around the wrong
criterion. It would be like grouping your CD collection by the colour of their
covers: doable, but not a useful relationship to create.
It is vital that you are forming that relationship around the right characteristics.
Quite often—and I have been guilty of it myself in the past—I have seen things
like this (and let’s imagine that the ...
denotes an omission of, say, 100
lines):
%brand-font {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
h1 {
@extend %brand-font;
font-size: 2em;
}
...
.btn {
@extend %brand-font;
display: inline-block;
padding: 1em;
}
...
.promo {
@extend %brand-font;
background-color: #BADA55;
color: #fff;
}
...
.footer-message {
@extend %brand-font;
font-size: 0.75em;
}
Which, of course, gives us this:
h1, .btn, .promo, .footer-message {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
h1 {
font-size: 2em;
}
...
.btn {
display: inline-block;
padding: 1em;
}
...
.promo {
background-color: #BADA55;
color: #fff;
}
...
.footer-message {
font-size: 0.75em;
}
The issue here is that I have forced a relationship between unrelated rules—that live hundreds of lines away from one another—based on shared traits that are purely coincidental. And not only have I forced an unusual relationship, but I now have a very unusual source order in which specificity is jumbled up. I am distributing selectors across my codebase for purely circumstantial reasons. This is not good news.
I have transplanted unrelated rulesets to hundreds of lines away from their
source, in order to live with other rulesets, in the incorrect location, based
on purely coincidental and circumstantial similarities. This is not a good
way to use @extend
. (In fact, this is probably a perfect use-case for an
argument-less mixin. We’ll come back to that soon.)
Another case of an abused @extend
looks a little like this:
%bold {
font-weight: bold;
}
...
.header--home > .header__tagline {
@extend %bold;
color: #333;
font-style: italic;
}
...
.btn--warning {
@extend %bold;
background-color: red;
color: white;
}
...
.alert--error > .alert__text {
@extend %bold;
color: red;
}
This, as you would expect, gives us the following:
.header--home > .header__tagline,
.btn--warning,
.alert--error > .alert__text {
font-weight: bold;
}
...
.header--home > .header__tagline {
color: #333;
font-style: italic;
}
...
.btn--warning {
background-color: red;
color: white;
}
...
.alert--error > .alert__text {
color: red;
}
This weighs 299 bytes.
Oftentimes, the selectors you’re transplanting may be longer than the declarations you’re trying to avoid repeating.
If we were to actually just repeat the font-weight: bold;
declaration
n times—instead of trying to avoid repeating it at all—we’d actually
achieve a smaller file size: 264 bytes. This is just a very timid model, but
it should help to illustrate the possibility for diminishing returns.
@extend
ing single declarations can often be counterproductive.
So, when do we use @extend
?
We’d use @extend
to share traits among explicitly related rulesets. A perfect
example:
.btn,
%btn {
display: inline-block;
padding: 1em;
}
.btn-positive {
@extend %btn;
background-color: green;
color: white;
}
.btn-negative {
@extend %btn;
background-color: red;
color: white;
}
.btn-neutral {
@extend %btn;
background-color: lightgray;
color: black;
}
Which results in:
.btn,
.btn-positive,
.btn-negative,
.btn-neutral {
display: inline-block;
padding: 1em;
}
.btn-positive {
background-color: green;
color: white;
}
.btn-negative {
background-color: red;
color: white;
}
.btn-neutral {
background-color: lightgray;
color: black;
}
This is a perfect use-case for @extend
. These rulesets are inherently related;
their shared traits are shared for a reason, not coincidentally. Further, we
aren’t transplanting their selectors hundreds of lines away from their source,
so our Specificity Graph
stays nice and sane.
The mixins without arguments are bad
rule is a well-meaning one, but
unfortunately it’s just not as simple as that.
This rule stems from a slight misunderstanding of the DRY principle. DRY is a principle that aims for a Single Source of Truth within a project. DRY is about not repeating Yourself, it is not about completely avoiding repetition.
If you manually type a declaration 50 times in a project, you are repeating yourself: this is not DRY. If you can generate that declaration 50 times without having to manually repeat it, this is DRY: you are generating repetition without actually repeating yourself. This is quite a subtle but important distinction to be aware of. Repetition in a compiled system is not a bad thing: repetition in source is a bad thing.
The Single Source of Truth means that we can store the source of a repeated construct in one place and recycle and reuse it without ever actually duplicating it. Sure, a system might repeat it for us, but its source only ever exists once. This means we can change it once and that change will propagate everywhere; that there will be no duplication of that construct in our source code; that there is a Single Source of Truth. This is what we mean when we talk about DRY.
With this in mind, we can begin to realise that mixins without arguments can
actually be very useful. Let’s go back to the %brand-font {}
example from
earlier on.
Let’s imagine we’re using a particular font in our project that must always be
defined alongside a specific font-weight
:
.foo {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
.bar {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
.baz {
font-family: webfont, sans-serif;
font-weight: 700;
}
It would get quite tedious to manually repeat those two declarations over and
over in our codebase; we’d have to remember the number 700
as opposed to the
more familiar regular
or bold
; and if we ever change the web font or its
weight, we’d have to go through the project and change it everywhere.
We covered earlier that we should not force relationships by using @extend
here, but what we probably should do is use a mixin:
@mixin webfont() {
font-family: webfont, sans-serif;
font-weight: 700;
}
...
.foo {
@include webfont();
}
...
.bar {
@include webfont();
}
...
.baz {
@include webfont();
}
Yes, this will compile to repetition. No, we are not repeating ourselves. It is important to remember here that these are unrelated rulesets, so we did not ought to make them related. They are unrelated and just happen to have some shared traits, so this repetition is sensible, and is to be expected. We want to use those declarations in n places, so we make them appear in n places.
Argument-less mixins are great for just spitting out repeated groups of identical declarations whilst maintaining a Single Source of Truth. See it like a Sassy extension of your copy/paste clipboard: you’re just using it to paste out a few strings you’ve stored elsewhere earlier on. We have our Single Source of Truth, which means we can propagate changes to these declarations whilst only ever making one manual change. Very DRY.
It is also probably worth noting that Gzip favours repetition, so that will almost entirely negate the costs of the slight added filesize.
Of course, mixins are also really, really useful for generating dynamic values within repeated constructs: mixins with arguments. I don’t think anyone could say these are a bad idea: they’re DRY but also allow us to make on-the-fly modifications to our Single Source of Truth. For example:
@mixin truncate($width: 100%) {
width: $width;
max-width: 100%;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.foo {
@include truncate(100px);
}
Spitting out the same declarations, but dynamically setting the value of
width
on a case-by-case basis.
This is the most common and widely agreed upon form of mixin, and I think we can all agree that these are a good idea.
Only use @extend
when the rulesets that you are trying to DRY out are
inherently and thematically related. Do not force relationships that do not
exist: to do so will create unusual groupings in your project, as well as
negatively impacting the source order of your code.
Use a mixin to either inject dynamic values into repeated constructs, or as a Sassy copy/paste which allows you to repeat the same group of declarations throughout your project whilst keeping a Single Source of Truth.
Use @extend
for same-for-a-reason; use a mixin for same-just-because.
Harry Roberts is an independent consultant web performance engineer. He helps companies of all shapes and sizes find and fix site speed issues.
Hi there, I’m Harry Roberts. I am an award-winning Consultant Web Performance Engineer, designer, developer, writer, and speaker from the UK. I write, Tweet, speak, and share code about measuring and improving site-speed. You should hire me.
You can now find me on Mastodon.
I am available for hire to consult, advise, and develop with passionate product teams across the globe.
I specialise in large, product-based projects where performance, scalability, and maintainability are paramount.