Smashing Newsletter

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.

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.

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.

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 @extend. 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:

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 combinator, such as >, +, ~ or the white space ancestor combinator. Many different types of relationship can exist in CSS, even not-so-obvious ones:

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:

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:

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.

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.

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.

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:

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.

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:

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:

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.

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.

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.

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:

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.

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() pseudoselector. 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:

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.