The Extend Concept

Published by Chris Coyier

All three of the most popular CSS preprocessors support extend. I don't have any data, but in my experience this feature is confusing at first and thus a bit under used. I thought we could talk a bit about how it works, why and when to use it, some gotchas, and what it might be like in the future.

The Basic Concept

Inheritance. I want this selector to inherit the styles from this selector.

Basic Usage

I'll use Sass here. We'll cover all the preprocessors later.

.foo {
  color: red;
}
.bar {
  @extend .foo;
}

Output:

.foo, .bar {
  color: red;
}

Note that it doesn't "go get" the styles from .foo and insert them below into .bar, thus duplicating the styles. Instead it comma separates the original selector, applying the styles to both more efficiently.

Relationship to Mixin

You can often achieve the same result with a mixin.

@mixin stuff {
  color: red;
}
.foo {
  @include stuff;
} 
.bar {
  @include stuff;
}

This will get those styles into both selectors. This is often easier to understand and honestly has less "gotchas," so is more common to see. Note that the output moves the styles into both places:

.foo {
  color: red;
}
.bar {
  color: red;
}

Which, while not that huge of a deal (especially since repetitive text is particularly easy to squish for gzip) it is less efficient.

Mixins can do things that extend cannot though, namely take parameters and process/use them in the output.

@mixin padMasterJ ($param: 10px) {
  padding: $param;
}

Extend can't do that, so a rule of thumb is: any time you'd use a mixin with no parameter, an extend will be more efficient. Of course there are exceptions to every rule, which we'll get into later.

Another thing to note is that all top-level selectors are extendable. LESS users will understand that more instinctively because in LESS all selectors are also mixins, which is weird to Sass users.

The Sass Way

We've used Sass so far, but let's note something specific about how Sass handles Extend. It extends all nested selectors as well.

.module {
  padding: 10px;
  h3 {
    color: red; 
  }
}

.news {
  @extend .module;
}

Outputs:

.module, .news {
  padding: 10px;
}
.module h3, .news h3 {
  color: red;
}

A limitation of Sass extend is that it cannot extend a nested selector. Here is an example of that:

.header {
  h3 {
    color: red; 
  }
}

.special-header {
  /* Error: can't extend nested selectors */
  @extend .header h3;
}

The LESS Way

LESS uses &:extend. To port our super simple example over, it would look like:

.foo {
  color: red;
}
.bar {
  &:extend(.foo);
}

Which outputs:

.foo,
.bar {
  color: red;
}

What is notable about LESS extend is that it doesn't extend nested selectors by default. So if we do this:

.module {
  padding: 10px;
  h3 {
    color: red; 
  }
}

.news {
  &:extend(.module);
}

The h3 will not be extended, and will output:

.module,
.news {
  padding: 10px;
}
.module h3 {
  color: red;
}

However, if you add the all keyword, like:

.news {
  &:extend(.module all);
}

It will extend everything:

.module,
.news {
  padding: 10px;
}
.module h3,
.news h3 {
  color: red;
}

You can also specify a nested selector to extend in place of the all keyword.

The Stylus Way

Stylus uses @extend and works largely similar to Sass.

.module 
  padding 10px
  h3 
    color red

.news 
  @extend .module

Outputs:

.module,
.news {
  padding: 10px;
}
.module h3,
.news h3 {
  color: #f00;
}

The one notable difference from Sass is that it can extend a nested selector. Like:

.header 
  padding 10px
  h3 
    color red

.special-header 
  @extend .header h3

Will get you:

.header {
  padding: 10px;
}
.header h3,
.special-header {
  color: #f00;
}

Extending Placeholders

Sass and Stylus have placeholder selectors. In Sass:

%grid-1-2 {
  float: left;
  width: 50%;
}

In Stylus:

$grid-1-2
  float left
  width 50%

This is such a tiny thing. It just doesn't output the placeholder in the CSS (hence, "placeholder") it just allows you to extend them. The big advantage here is you can use internal naming schemes that don't effect your external naming schemes. Grid classes are a good example of this. Names like "grid-1-2" and "grid-1-3" are great names internally, but not great actual HTML class names. With placeholders you can keep them internal.

.main-content {
  @extend %grid-2-3;
}
.sidebar {
  @extend %grid-1-3;
}

Watch Out for Selector Order

Because of the selector re-writing, you may occasionally run into a case where the selector is re-written earlier than another selector which overrides it. Note that the specificity will never change, but when the specificity is exactly the same on two selectors, the one further down in the final CSS "wins."

For instance:

.one {
   color: red;
}
.two {
   color: green;
}
.three {
   @extend .one;
}

Then you have an element like this:

<div class="three two">test</div>

You might assume the class name three "wins" because it's the lowest down in the Sass, extends .one, and thus the color will be red. But .three actually gets re-written up above .two:

.one, .three {
  color: red;
}

.two {
  color: green;
}

Since the element also has the class name .two, the color is actually green in the final rendering.

Watch Out For Mega Output

Here's a very reasonable scenario covered by Nicole Sullivan:

  1. You create a placeholder class for clearfix
  2. You create a generic module class that extend clearfix
  3. You create other module classes that extend module ("chaining" extends)
%clearfix {
  &:before,
  &:after {
    content: " ";
    display: table;
  }
  &:after {
    clear: both;
  }
}

.module {
  padding: 10px;
  @extend %clearfix;
  h3 {
    color: red;
    @extend %clearfix;
    span {
      float: right; 
    }
  }
}

.sports {
  @extend .module;
}

.news {
  @extend .module; 
}

The output for just the clearfix starts looking a little thick:

.module:before, .sports:before, 
.news:before, .module h3:before, 
.sports h3:before, .news h3:before, 
.module:after, .sports:after, 
.news:after, .module h3:after, 
.sports h3:after, .news h3:after {
  content: " ";
  display: table;
}
.module:after, .sports:after, 
.news:after, .module h3:after, 
.sports h3:after, .news h3:after {
  clear: both;
}

And that's just a couple of classes. I'm sure a whole big project could get way hairier. Not that it's not going to work, it's just you might be better of with an actual class and dropping it into the HTML as needed. Or a mixin might even produce less code in some cases depending on how deep the nesting goes.

The rule of thumb being: chose the technique that requires the least final output (or that works best for you).

Watch Out for Media Queries

None of the languages allow you to extend from inside a media query to a selector defined outside a media query.

This is OK:

@media (max-width: 100px) {
  .module {
    padding: 10px;
  }
  .news {
    @extend .module;
  }
}

This will not work:

.module {
  padding: 10px;
}

@media (max-width: 100px) {
  .news {
    @extend .module;
  }
}

It is the nature of the selector moving/rewriting. If you moved that selector out of the query, you are no longer honoring the author intent (it would apply regardless of the media query). Perhaps the rules could be copied inside, but then that doesn't honor the intent of extend. Sass 3.3 will have a way to deal with it, but the real answer as native browser support.

The Future of Extend

Extend is pretty powerful at the preprocessor layer, but would be way more powerful at the browser level. No more comma-separated selectors, mega output, source order concerns, media query problems... none of that. The browser just knows to apply the rules from the other selector to this one. From what I hear the powers that be needed some convincing this was a good idea, but now have been convinced, and it's a matter of writing up all the specs and whatnot. But I have no evidence of that to link to.

Perhaps it could be:

.module {
  padding: 10px;
}
.news {
  !extend .module;
}

I'm not sure... I doubt @extend could be used because those @words have kinda special meaning already (they wrap blocks conditionally). And functions like extend() are generally for values not properties. Kinda new/weird territory for the native language.

I would love to hear all your thoughts on extend. If you use it, how you use it, other problems you've found, how you imagine it working natively, etc.