Menu Search
Jump to the content X X
Smashing Conf New York

We use ad-blockers as well, you know. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. upcoming SmashingConf Barcelona, dedicated to smart front-end techniques and design patterns.

Extending In Sass Without Creating A Mess

The @extend directive in Sass is a powerful directive that facilitates the sharing of rules and relationships between selectors. However, it can produce undesirable side effects if it is not carefully implemented. Thankfully, there are many strategies for using @extend effectively that can prevent these side effects and produce clean, organized CSS.

By examining @extend in detail and exploring these various strategies, you can:

  • accurately predict exactly what happens when you use @extend,
  • and make more informed decisions about when to use a @mixin and when to use @extend, to ensure optimal organization and to restrict unused styles in your style sheets.

What Happens When You Use @extend?

Sass’ @extend directive1 does much more than group extended selectors together, as the documentation suggests. Two main things happen when you @extend a selector:

  • the target “simple selector2 is identified;
  • constraints are formed on the target selector using:
    • the selector that’s doing the extending,
    • and the selector that’s being extended.

Let’s break this down with an example. Consider the following code:

.foo.bar { color: green; }

.ancestor .descendant { @extend .bar; } // .bar is being extended
  • The target simple selector is identified as .descendant.
  • Two constraints are formed:
    • .bar (and whatever extends it) must have the class .foo to be green,
    • .descendant must be a descendant of .ancestor.

Using this information, the @extend directive produces this ruleset:

.foo.bar, .ancestor .foo.descendant { color: green; }

The target selector .descendant now fulfills the constraint requirements: It has a class of .foo, and it is an ancestor of .ancestor.

Some common @extend scenarios are listed below. Notice the constraints (requirements) implicitly placed on the target selector and how these constraints are fulfilled in the outputted CSS:

// Extending a selector with an ancestor
.first { color: green; }
.second { @extend .first; }
.ancestor .first { background: white; }

// CSS
.first, .second { color: green; }
.ancestor .first, .ancestor .second { background: white; }

// Extending a selector with a descendant
.first .descendant { color: green; }
.second { @extend .first; }

// CSS
.first .descendant, .second .descendant { color: green; }


// Extending a selector within a compound selector
.one.two { color: green; }
.three { @extend .one; }

// CSS
.one.two, .two.three { color: green; } 


// Extending a selector using a complex selector
.foo.bar { color: green; }
.complex .selector { @extend .bar; }

// CSS
.foo.bar, .complex .foo.selector { color: green; }

// Alternating ancestors
.ancestor-1 .foo { color: green; }
.ancestor-2 .bar { @extend .foo; }

// CSS
// Notice how the ancestors switch places to 
// maintain the general ancestral constraints
.ancestor-1 .foo,
.ancestor-1 .ancestor-2 .bar,
.ancestor-2 .ancestor-1 .bar {
  color: green;
}

// Combinator with common ancestor
.foo.bar + .sibling { color: green; }
.foo .descendant { @extend .sibling; }

// CSS
// Notice how .foo is repeated to preserve
// constraint of .foo.bar
.foo.bar + .sibling,
.foo .foo.bar + .descendant {
  color: green;
}

There are a few takeaways from the examples above:

  • Extending obeys selector constraints.
    In the last example, .descendant is a sibling of .foo.bar, not .foo. It’s also a descendant of .foo, and since .foo can’t be both an ancestor and a sibling, it is outputted twice to maintain those respective constraints.
  • Ancestor and descendant selectors are prone to permutation explosion.
    Ancestors (in the form of .ancestor .descendant) can be anything from parents to grandparents and beyond. When selectors with ancestors extend other selectors with ancestors, all of these permutations must be accounted for.
  • Extending is a one-way operation.
    When .bar extends .foo, .bar is intimately aware of the relationships between .foo and its selectors, but not the other way around. And yes, selectors can reciprocally @extend each other.

Why Use @extend?

The @extend and @mixin directives are not exactly the same. Whereas a @mixin outputs rules that can be copied for multiple selectors and customized via parameters, @extend uses the mechanism described above for rule-sharing that allows it to serve three main purposes:

  • inheritance of rulesets between selectors that can be overridden by the extending selector;
  • traits, or shareable rulesets;
  • relationships between selectors that are maintained when a selector with a defined relationship is extended via combinators (for example, >, +, ~, etc.).

Strategies And Guidelines For @extend

Now that we know how to use @extend and what exactly causes selector explosion, let’s look at some strategies we can implement to properly share styles across selectors.

Use and Extend %placeholder Selectors.

Sass includes a special type of selector called the placeholder selector that is quite literally meant to be used with @extend3. These are very versatile selectors in that they:

  • can be extended, of course;
  • are ignored in CSS output, which can prevent unnecessary output;
  • can be dynamically declared just like any other selector in Sass, meaning you can %do-#{$this}, which gives it a leg up on non-dynamic mixins.

When you @extend %placeholders, the explosive side effects are lessened because you have one less shared selector to combine with in the final CSS output.

Extend a Selector as Few Times as Possible.

Consider the following example of inheritance by extending a parent (placeholder) selector:

// Don't do this:

%button {
  // … parent button styles
}

.button {
  @extend %button;
  // … button styles
}

.button-primary {
  @extend %button;
  // … primary button styles
}

.button-secondary {
  @extend %button;
  // … secondary button styles
}

// CSS - we want to avoid this:
.button, .button-primary, .button-secondary {
  // button styles
}

.button-primary { /* … */ }
.button-secondary { /* … */ }

When extending the same selector multiple times, ask yourself, What do all these extending selectors have in common? Then, you can limit how often a selector is extended by refactoring using a common selector, such as .button.primary, instead of .button-primary, so that the only extension is .button { @extend %button; }.

Extend Traits Selectively, and Consider Mixins for Short Traits.

The previous strategy can be difficult to accomplish with traits, which by definition can be shared among many selectors that are otherwise unrelated. Traits, in general, do not have hierarchical constraints; that is, they (usually) are agnostic to the relationship between the selector using it and other selectors. Therefore, they can be used with @extend or @mixin.

I would (personally) choose to @extend a trait with many rules. The outputted CSS will group unrelated selectors with the trait ruleset, although this isn’t a huge concern — style sheet maintenance occurs in your .sass or .scss files, not your .css files, and there are few performance trade-offs, in general.

On the other hand, I would use @mixin for rulesets that are terse and frequently used, such as clearfix. Extending it would cause the CSS to output a large number of selectors for this small trait. Using it as a mixin will make the outputted CSS much more readable, and repetition is OK because the ruleset is small.

So, here are some general guidelines for using traits:

  • Extend traits that have long rulesets or are infrequently used.
    This keeps your outputted CSS DRY and helps to group rulesets in your CSS for increased clarity in browser debugging.
  • Mixin traits that are more generalized, short and frequently used.
    You can @extend these types of traits, but you’re more at risk of selector explosion.
  • Stick repeated rulesets in a %placeholder trait or a @mixin.
    This is just a general good practice to keep your SCSS as DRY as possible.

Use @extend for Relationships.

This is one of the most important uses of @extend. Relationships between selectors in your style sheets can get quite complex, and @extend is the perfect tool for the job — you cannot express relationships with @mixin alone. A relationship between selectors is expressed with a combinator4, such as >, +, ~ or the white space ancestor combinator. Many different types of relationship can exist in CSS, even not-so-obvious ones:

  • .parent > .child
  • .ancestor .descendant
  • .sibling ~ .sibling
  • .sibling + .adjacent-sibling
  • .sibling + * ~ .non-adjacent-sibling
  • .parent > * > .grandchild
  • .parent > * .non-child-descendant
  • .uncle ~ * > .nephew
  • * + *, the any-adjacent-sibling relationship, or the lobotomized owl selector5

CSS enables us to define styles for selectors based on their relationships with other selectors. These relationship-defining selectors that use combinators are called complex selectors, and they have higher specificity than their standalone simple or compound selector parts. Styles defined as a relationship, therefore, have greater prevalence than styles defined in standalone selectors.

The following strategies address how to extend with relationships in depth.

Define Relationships via Placeholders, Not Classes.

Remember what happens when you have two ancestral relationships (such as .foo .bar {…} .baz .qux { @extend .bar; })? Being aware that multiple extended ancestral relationships are prone to selector explosion, we should always define our relationships once, and keep them one-sided. And what better place to define relationships than in %placeholder selectors, to reduce CSS output?

Let’s say you want buttons to be styled a little differently in modals, and you want a little space between adjacent buttons. Here’s how that relationship would look like using placeholders:

%button {
  // … button styles

  + %button {
    margin-left: 1rem;
  }
}

%modal {
  // … modal styles
  
  %button {
    // … modal-specific button styles
  }
}

// "Exported" classes
.button { @extend %button; }
.modal { @extend %modal; }

Notice that two relationships are defined: the sibling %button + %button relationship and the ancestral %modal %button relationship. These relationships are defined only once in the placeholders, and the outputted CSS is exactly what we’d expect:

.button { /* … button styles */ }
.button + .button { margin-left: 1rem; }

.modal { /* … modal styles */ }
.modal .button { /* … modal-specific button styles */ }

Represent One Selector Per Placeholder in Relationships.

This follows the first strategy: Extend a selector as few times as possible. When writing markup and CSS selectors, having as few selectors represent an entity as possible will prevent selector explosion, even if it means you have to add a semantic class to the markup to eliminate selectors.

%button { // … button styles }

// Avoid this:
button, input[type="button"], input[type="submit"], .button {
  @extend %button;
}

// Try this instead, and add the class .button
// for every type of button in the HTML
.button {
  @extend %button;
}

Be Wary of Descendant and General Sibling Combinators.

These two combinators — .ancestor .descendant and .sibling ~ .general-sibling, respectively — are the most prone to selector explosion. Why? Their constraints are less strict than other combinators. A descendant can be a child, grandchild, etc. A general sibling can be adjacent, one ahead, two ahead, etc.

If two complex selectors both have descendant or general sibling combinators, then all possibilities must be accounted for in the outputted CSS. That’s why you should make sure that only the extendee or extender has a descendant or general sibling combinator, not both. Following the guidelines above on extending only from %placeholders is a good way to enforce this.

%modal {
  // … modal styles
  
  // Relationship: a %button that is a descendant of %modal
  %button { /* … modal-button styles */ }
}

// Don't do this!
.modal {
  @extend %modal;

  // This is unnecessary - the relationship
  // is already defined!
  .button { @extend %button; }
}

// Do this - keep it simple!
.modal { @extend %modal; }
.button { @extend %button; }

Have Separate Placeholders for Modifiers.

By having separate %placeholders to represent modified components, you can extend without the risk of confounding compound and/or complex selectors. For example, @extend %button%active; is risky, but @extend %button-active; is not.

%button {
  // … button styles

  &-active { // same as %button-active
    // … active button styles
  }

  &-disabled {
    // … disabled button styles
  }
}

.button {
  @extend %button;

  &.active { @extend %button-active; }
  &:disabled { @extend %button-disabled; }
}

Use @extend Inside Similar Media Queries.

You’re probably thinking, “I thought that using @extend in media queries is currently not possible.” This is partially true, but also slightly misleading. The truth is that you can only extend a selector in the same media query, and, for all intents and purposes, this is sufficient.

What is important, however, is that your selectors are meaningful, in that you can declare them with proper intent. If you are trying to extend an outside selector inside a media query, that’s a clear conflict of interests — one selector’s intent is to be generally applied, while the other’s intent is to be applied only within the current media context.

For instance, the SCSS below will not work because of the media query restriction and, more importantly, because the intent is not clear:

%panel {
  display: block;
  float: left;
  width: 100%;
  // … other panel styles
}

%panel-half { width: 50%; }

.panel {
  @extend %panel;

  &.half { @extend %panel-half; }
}

// This will NOT work and will throw this error:
// You may not @extend an outer selector from within @media.
// You may only @extend selectors within the same directive.
@media (max-width: 700px) {
  .panel-mobile {
    @extend %panel;

    &.half { @extend %panel-half; }
  }
}

So, what went wrong here? In defining %panel, the intent was, “This panel should have this styling in general,” whereas we were trying to export .panel-mobile as “This panel should have this styling only in this media query context.” This is a clear miscommunication.

The solution to this is to match extendee intent with extender intent, especially regarding @media queries. In other words, if you want an extender to carry certain styling only for a specific media query, it should extend only within the same @media query or, alternatively, break out of the @media query with @at-root.

Let’s see how we can revise our code with the above two suggestions:

// Solution 1: Extend within the same media query
%panel { /* … */ }
%panel-half { /* … */ }

// Define extendees with the same intent as extenders
@media (max-width: 700px) {
  %panel-mobile { /* … */ }
  %panel-mobile-half { /* … */ }
}

.panel {
  @extend %panel; 

  &.half { @extend %panel-half; }

  // This WILL work!
  @media (max-width: 700px) {
    &-mobile {
      @extend %panel-mobile;

      &.half { @extend %panel-mobile-half; }
    }
  }
}
// Solution 2: Break out of '@media' with '@at-root'
%panel {
  display: block;
  float: left;
  // … other panel styles
}

// Separation of intent
%panel-full { width: 100%; }
%panel-half { width: 50%; }

@media (max-width: 700px) {
  %panel-mobile-full { width: 100%; }
  %panel-mobile-half { width: 50%; }
}

.panel {
  @extend %panel;

  &.half { @extend %panel-half; }

  @media (max-width: 700px) {
    &-mobile, &-mobile-half {
      // General panel styling intent is maintained
      @at-root (without: media) { @extend %panel; }
    }

    &-mobile {
      @extend %panel-mobile-full;

      &.half { @extend %panel-mobile-half; }
    }
  }
} 

Use @extend With :matches() in the Future.

Sometimes, it’s difficult to adhere to the guideline that you should @extend a selector as few times as possible. Take the HTML headings, for instance: h1, h2, h3, h4, h5, h6. There are six of them, and they all can’t be selected with a single simple selector. One solution would be to give them all a class of .heading, but if you’re building for the future, there’s a better way.

Enter CSS 4’s :matches() pseudoselector6. This selector allows you to somewhat mimic @extend functionality in native CSS, without the selector explosion (which we’ve learned how to avoid). The best part is, Sass plays nicely with :matches() and related pseudoselectors. Let’s style our headings to demonstrate:

// With :matches()
:matches(%heading) {
  // … heading styles
}

// Relationship: any heading that is a descendant of %section-foo
%section-foo :matches(%heading) { /* … */ }

h1, h2, h3, h4, h5, h6 {
  @extend %heading;
}

.section-foo { @extend %section-foo; }

// CSS output - much better than this selector explosion:
// .section-foo h1, .section-foo h2, .section-foo h3, .section-foo h4, .section-foo h5, .section-foo h6
.section-foo :matches(h1, h2, h3, h4, h5, h6) {
  // … section-foo heading style
}

Today, you’ll have to use :-webkit-any() and :moz-any() in place of :matches() in WebKit browsers and Firefox, respectively, until they natively support :matches(). With Internet Explorer, you’re out of luck (no surprise there). This is a good guideline to keep in the back of your mind until CSS 4 is implemented in all modern browsers.

Conclusion

The @extend directive is nothing to fear, and it’s very versatile. Understanding how @extend works and keeping the guidelines above in mind will enable you to use @extend to its full advantage — reducing CSS output and keeping relationships intact, no matter what exported selectors (classes, attributes, etc.) you use. Make wise use of both the @mixin and @extend directives — they’re meant to coexist in your well-organized style sheets.

(rb, al, ml)

Footnotes Link

  1. 1 http://sass-lang.com/documentation/file.SASS_REFERENCE.html#extend
  2. 2 http://dev.w3.org/csswg/selectors-4/#simple
  3. 3 http://sass-lang.com/documentation/file.SASS_REFERENCE.html#placeholder_selectors_
  4. 4 http://dev.w3.org/csswg/selectors-4/#combinator
  5. 5 http://alistapart.com/article/axiomatic-css-and-lobotomized-owls
  6. 6 http://dev.w3.org/csswg/selectors-4/#matches-pseudo
SmashingConf New York

Hold on, Tiger! Thank you for reading the article. Did you know that we also publish printed books and run friendly conferences – crafted for pros like you? Like SmashingConf Barcelona, on October 25–26, with smart design patterns and front-end techniques.

↑ Back to top Tweet itShare on Facebook

David Khourshid is a front-end web developer and speaker in Orlando, Florida. He is passionate about JavaScript, Sass, and cutting-edge front-end technologies. He is also a pianist and enjoys mathematics, and is constantly finding new ways to apply both math and music theory to web development. You can check out his work on Codepen.

  1. 1

    Thanks for the detailed article, David.

    Extends have been an enemy of mine for the past few years, especially when working on big projects. They brought their lot of scalability issues and made styles hard to debug on these large codebases. But all projects are unique, so don’t take my word for it.

    Do you think it’s possible that there is a point until which you can use @extend, but then it turns into an anti-pattern when the codebase reaches a certain size?

    1
    • 2

      David Khourshid

      May 4, 2015 2:38 pm

      Hey, Kaelig!

      It’s worth noting that using @extend is most effective with a CSS organization that’s component-based. This way, there are very few page-specific styles, and much more flexibility (including the low specificity for each component).

      I have been using this in large-scale projects for the last year, without any issues. It scales well, as long as the project continues to be component-based. Thankfully, this is where front-end web development is trending now (a la React, etc.).

      2
  2. 3

    Robert Smith

    May 4, 2015 2:16 pm

    I’m going to post a quote here from a post by Harry Roberts because I agree with him, @extend tends to cause more problems than it solves:

    “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.”

    As Harry wrote it @extend can be useful, but this is what makes it dangerous, it’s like Crockford put in his JS the Good Parts book – The worst parts of the language are those that are useful but at the same time are often a major cause of unwanted bugs and because the features are sometimes useful (As mentioned in the above post) it becomes difficult to convince other developers to avoid them.

    I believe @extend to be one of these. Since I stopped using @extend i’ve not had to deal with the bugs associated with the strong, unwanted relationships between what would be totally unrelated selectors. It’s possible to code around this but for what it’s worth I think @extend is just best avoided.

    2
    • 4

      David Khourshid

      May 4, 2015 2:34 pm

      Hey Robert –

      There will be developers who will shy away from using @extend for fear of unwanted side-effects, that’s a given. My article hints at using a component-centric architecture for stylesheets, which allows specificity to be very low and @extend to be used in a non-destructive manner.

      Ultimately, it’s your choice to use @extend thoughtfully or opt for using @mixins instead. Use whichever you can develop in more efficiently.

      Thanks for your comment!

      1
      • 5

        Robert Smith

        May 4, 2015 2:58 pm

        I see, It’s good that your article is explaing how to use @extend in a sensible way that is less likley to cause issue. Of course we should be pragmatic in our approach and use the tools at our disposal that is best suited for the situation – for components like you describe maybe it is @extend.
        I tend to stay away from them mostly because other developers use the codebase and there’s less of a chance of them screwing things up this way. Many are not aware of the dangers of @extend so I put a blanket ban on it just for simplicity.

        1
        • 6

          Raymond Lopez

          May 5, 2015 5:24 am

          Well said @Robert, Simplicity :)

          2
        • 7

          Matthew Trow

          May 5, 2015 9:38 am

          Totally agree. @extend can add needless complexity to code, making it more difficult to follow, especially when multiple developers are working from the same code base.

          2
  3. 8

    Jaime Caballero

    May 4, 2015 2:21 pm

    Nice one mate!
    It’s good to see somebody talking about @extend, I believe people underestimate its capabilities.
    I built a Sass mixin that lets you set a selector budget on @extend, in case somebody is interested: http://jaicab.com/sass-ext/
    Awesome CSS4 selectors too!

    1
  4. 9

    I think there is a typo. In your first example, the @extend directive should produce this ruleset:

    .foo.bar, .ancestor .foo.descendant {color: green;}

    and not:

    .foo.bar, .descendant .foo.ancestor { color: green; }

    1
    • 10

      David Khourshid

      May 4, 2015 2:34 pm

      Thanks, Ankur! I’ll get that fixed as soon as possible.

      0
  5. 11

    Ilkka Sopanen

    May 4, 2015 2:26 pm

    Hi David!

    Thanks for the article. Just a small correction. I’m quite sure this…

    .foo.bar, .descendant .foo.ancestor { color: green; }

    ..should be…

    .foo.bar, .ancestor .foo.descendant { color: green; }

    …right?

    0
  6. 14

    Dylan Parry

    May 4, 2015 3:05 pm

    Interesting article, but far too much for me to take in all at once… will have to have a few reads! One thing jumped out at me as “not quite right” though…

    .uncle ~ * > .cousin

    Should be:

    .uncle ~ * > .nephew

    Unless I’m reading it wrong?

    0
  7. 16

    Benjamin Solum

    May 4, 2015 8:44 pm

    I’ve been a huge proponent of @extends w/Placeholders for awhile now; assuming you subscribe to certain CSS conventions to keep things organized.

    However, I’ve been playing around with Clean-css (with the grunt-contrib-cssmin module) and I think for future projects I’ll just skip over this feature as Clean-css walks your CSS and does all of the selector merging for you so you don’t have to worry about shooting yourself in the foot with the potential pitfalls of @extend.

    3
  8. 17

    How about a situation when aiming at semantic naming (e.g BEM)? You seem to prefer .button.primary over .button–primary, but what if we want a .login__button over a .button.primary.small.block? Isn’t the %placeholders for different buttons colors, sizes and behaviour a way to go?
    Also, what about %placeholders for grid columns when writing semantic css? Isn’t it DRY-er to @extend %col-6 in your .content and .contact-form, and let SASS aggregate those in output, rather than use a @mixin in each for making a col-6 size, and as a result have the code repeated in output?

    5
    • 18

      Great questions, Julia!

      Yes, placeholders can (and should) be used for “traits”. Then, you can have .login__button extend the relevant traits. My style is to have all buttons be able to share a common selector (makes sense, right?) such as .button, but for BEM, you can just target each individual selector.

      My rule of thumb for extending vs. using a mixin for short traits such as %col-6 is to see if the extending selectors are *longer* than the rules. If they are (e.g. .super-long-selector is longer than “width: 50%”), I opt for using a mixin to cut down on stylesheet size.

      Of course, if your stylesheets are minified and GZIPped, this is a moot point. You can absolutely @extend %col-6 if you’d like.

      1
  9. 19

    Awesome read! I learn’t so many things. Keep writing :)

    1
  10. 21

    Hi,

    you wrote this:

    %button { // … button styles }

    // Avoid this:
    button, input[type=”button”], input[type=”submit”], .button {
    @extend %button;
    }

    // Try this instead, and add the class .button
    // for every type of button in the HTML
    .button {
    @extend %button;
    }

    What is the reason of defining this placeholder if i only use this code in the button-class? Why not writing this code directly in the button-class? (same results but less code)

    1
  11. 22

    xLondonEscorts

    August 20, 2015 3:25 pm

    Sounds interesting … I will follow the new posts indeed.. Keep the good work and sharing info mate :)

    2

↑ Back to top