@extend: the ex

David Khourshid, Counsyl

@davidkpiano

About me

What is @extend?

 ¯\_(ツ)_/¯ 

What we think @extend is

  • Sharing styles
  • An alternative to @mixins
  • "Inheritance"
  • Magic selector string replacement
  • Magic

Story time.

"I should make my own grid system."

- Every Sass developer, ever

.col-1, .col-3, .col-8, etc.
@extend %column;
@extend %row;
.row-1, .row-3, .row-8, etc.
%column, %row { @extend %grid; }

You can see where

this is going.

.cols-0 > *, .columns-0 > *, .col-0, .column-0, .cols-offset-0 > *, .columns-offset-0 > *, .col-offset-0, .column-offset-0, 
.cols-1 > *, .columns-1 > *, .col-1, .column-1, .cols-offset-1 > *, .columns-offset-1 > *, .col-offset-1, .column-offset-1,
 .cols-2 > *, .columns-2 > *, .cols-6th > *, .columns-6th > *, .col-6th, .column-6th, .col-2, .column-2, .cols-offset-2 > *,
 .columns-offset-2 > *, .cols-offset-6th > *, .columns-offset-6th > *, .col-offset-6th, .column-offset-6th, .col-offset-2, 
.column-offset-2, .cols-3 > *, .columns-3 > *, .cols-4th > *, .columns-4th > *, .col-4th, .column-4th, .col-3, .column-3,
 .cols-offset-3 > *, .columns-offset-3 > *, .cols-offset-4th > *, .columns-offset-4th > *, .col-offset-4th, .column-offset-
4th, .col-offset-3, .column-offset-3, .cols-4 > *, .columns-4 > *, .cols-3rd > *, .columns-3rd > *, .col-3rd, .column-3rd, 
.col-4, .column-4, .cols-offset-4 > *, .columns-offset-4 > *, .cols-offset-3rd > *, .columns-offset-3rd > *, .col-offset-3rd,
 .column-offset-3rd, .col-offset-4, .column-offset-4, .cols-5 > *, .columns-5 > *, .col-5, .column-5, .cols-offset-5 > *, 
.columns-offset-5 > *, .col-offset-5, .column-offset-5, .cols-6 > *, .columns-6 > *, .cols-half > *, .columns-half > *, .col-
half, .column-half, .col-6, .column-6, .cols-offset-6 > *, .columns-offset-6 > *, .cols-offset-half > *, .columns-offset-half
 > *, .col-offset-half, .column-offset-half, .col-offset-6, .column-offset-6, .cols-7 > *, .columns-7 > *, .col-7, .column-7,
 .cols-offset-7 > *, .columns-offset-7 > *, .col-offset-7, .column-offset-7, .cols-8 > *, .columns-8 > *, .col-8, .column-8, 
.cols-offset-8 > *, .columns-offset-8 > *, .col-offset-8, .column-offset-8, .cols-9 > *, .columns-9 > *, .col-9, .column-9, 
.cols-offset-9 > *, .columns-offset-9 > *, .col-offset-9, .column-offset-9, .cols-10 > *, .columns-10 > *, .col-10, .column-
10, .cols-offset-10 > *, .columns-offset-10 > *, .col-offset-10, .column-offset-10, .cols-11 > *, .columns-11 > *, .col-11, 
.column-11, .cols-offset-11 > *, .columns-offset-11 > *, .col-offset-11, .column-offset-11, .cols-12 > *, .columns-12 > *, 
.col-12, .column-12, .cols-offset-12 > *, .columns-offset-12 > *, .col-offset-12, .column-offset-12, .cols-5th > *, .columns-
5th > *, .col-5th, .column-5th, .cols-offset-5th > *, .columns-offset-5th > *, .col-offset-5th, .column-offset-5th, .cols-7th
 > *, .columns-7th > *, .col-7th, .column-7th, .cols-offset-7th > *, .columns-offset-7th > *, .col-offset-7th, .column-
offset-7th, .cols-8th > *, .columns-8th > *, .col-8th, .column-8th, .cols-offset-8th > *, .columns-offset-8th > *, .col-
offset-8th, .column-offset-8th, .cols-9th > *, .columns-9th > *, .col-9th, .column-9th, .cols-offset-9th > *, .columns-
offset-9th > *, .col-offset-9th, .column-offset-9th {
  box-sizing: border-box;
  margin: 0;
  display: block;
  position: relative;
  float: left;
  clear: none;
  height: 100%;
}

Denial

 

Anger

 

Bargaining

 

Depression

 

Acceptance

 

Denial

"This isn't my fault. There's a bug with Sass."

Anger

"The @extend directive is literally the worst."

Bargaining

"I'll just use @mixins instead."

Depression

"I wish I were better at Sass and understood all its features."

Acceptance

"Okay, I'll sit down and take the time to learn about @extend."

This is where the Sass community generally is.

F.U.D.

@extend is very simple (in theory) and very powerful, when used properly.

 

 

We need to eliminate the FUD.

.btn-group-vertical>.btn:not(:first-child):not(:last-child),
.btn-group-vertical>.col-signup .signup .btn-facebook:not(:first-child):not(:last-child),
.btn-group-vertical>.col-signup .signup .btn-google:not(:first-child):not(:last-child),
.btn-group-vertical>.col-signup .signup .btn-login:not(:first-child):not(:last-child),
.btn-group-vertical>.col-signup .signup .btn-signup:not(:first-child):not(:last-child),
.btn-group-vertical>.frm-no-account .btn-go:not(:first-child):not(:last-child),
.btn-group-vertical>.nav-bar .menu-search li .btn-close:not(:first-child):not(:last-child),
.btn-group-vertical>.nav-bar .menu-search li .btn-search:not(:first-child):not(:last-child),
.btn-group-vertical>div[chart-view] .tablet-switch .first-half-btn:not(:first-child):not(:last-child),
.btn-group-vertical>div[chart-view] .tablet-switch .second-half-btn:not(:first-child):not(:last-child),
.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,
.btn-group>.btn-group:not(:first-child):not(:last-child)>.col-signup .signup .btn-facebook,
.btn-group>.btn-group:not(:first-child):not(:last-child)>.col-signup .signup .btn-google,
.btn-group>.btn-group:not(:first-child):not(:last-child)>.col-signup .signup .btn-login,
.btn-group>.btn-group:not(:first-child):not(:last-child)>.col-signup .signup .btn-signup,
.btn-group>.btn-group:not(:first-child):not(:last-child)>.frm-no-account .btn-go,
.btn-group>.btn-group:not(:first-child):not(:last-child)>.nav-bar .menu-search li .btn-close,
.btn-group>.btn-group:not(:first-child):not(:last-child)>.nav-bar .menu-search li .btn-search,
.btn-group>.btn-group:not(:first-child):not(:last-child)>div[chart-view] .tablet-switch .first-half-btn,
.btn-group>.btn-group:not(:first-child):not(:last-child)>div[chart-view] .tablet-switch .second-half-btn,
.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>.col-signup .signup .btn-facebook:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>.col-signup .signup .btn-google:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>.col-signup .signup .btn-login:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>.col-signup .signup .btn-signup:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>.frm-no-account .btn-go:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>.nav-bar .menu-search li .btn-close:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>.nav-bar .menu-search li .btn-search:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>div[chart-view] .tablet-switch .first-half-btn:not(:first-child):not(:last-child):not(.dropdown-toggle),
.btn-group>div[chart-view] .tablet-switch .second-half-btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
  border-radius: 0
}

Let's avoid this.

And this.

What is @extend?

What does @extend do?

A {
  @extend B;
}

"Style elements matching A

as if they matched B."

That's it. Seriously.

A {
  @extend B;
}

The extending selector

The selector being extended

  • Can be any type of selector (simple, compound, complex, list)
  • Can only be a simple selector!

 

  • Simple - single selector
  • Compound - simple selectors joined together, not separated
  • Complex - simple or compound selectors separated by combinators
  • List - more than one selector separated by commas
div { /* ... */ }

.class { /* ... */ }

:before { /* ... */ }

#an-id { /* ... */ }

[type] { /* ... */ }
div.class { /* ... */ }

.class.another-class { /* ... */ }

div.class:before { /* ... */ }

#an-id.class { /* ... */ }

div#an-id.class[type] { /* ... */ }
div p { /* ... */ }

.class + .child { /* ... */ }

div > p:before { /* ... */ }

#an-id ~ div > .class { /* ... */ }

[type] * > * { /* ... */ }
div, p, button { /* ... */ }

.class, .another-class { /* ... */ }

:before, :after, * { /* ... */ }

#an-id, #another-id > .complex { /* ... */ }

[type], .compound.selector { /* ... */ }

Selector

Types

What causes selector explosion?

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

// 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;
}

How did this happen?

  • .ancestor-1 is an ancestor of .foo
  • .ancestor-2 is an ancestor of .bar
  • .bar (which has an ancestor of .ancestor-2) extends .foo
  • Now, .bar has ancestors .ancestor-1 and .ancestor-2

@extend is not a mind-reader.

The most probable selectors are output.

What causes selector explosion?

  • Using the general descendant combinator
  • Using the general sibling combinator
  • Long complex selectors
    • (i.e., nesting)
  • Extending the same selector multiple times
.ancestor .descendant {
  /* ... */
}

.foo {
  @extend .descendant;
}
.sibling ~ .sibling {
  /* ... */
}

.foo {
  @extend .sibling;
}
.ancestor {
  .descendant {
    section > div {
      /* ... */
    }
  }
}

.foo {
  @extend div;
}
.ancestor {
  .descendant {
    section > div {
      /* ... */
    }
  }
}

.foo, .bar, .baz {
  @extend div;
}

Loose constraints.

Selector explosion is a side-effect of loose architecture.

Why can't we just use a @mixin?

                {




}

                {




}

                {




}
                ,                
                ,
                {




}

@mixin

@extend

@mixin inserts 

declaration blocks

inside rulesets.

@extend appends unified selectors to selector lists.

Thanks @una for the illustration idea!

@mixin


@extend

"Here are some highly customizable declarations that we can stamp wherever we want."

"I'm inheriting styles and relationships from a component that is an integral part of our pattern library."

Occasionally, you'll have to pick your poison and choose one or the other.

Use and @extend %placeholder selectors.

.modal {
  > .button {
    // ...
  }
}

.modal-button {
  @extend .modal > .button;
}

Can't do this! (error)

Separate out the concept of a "modal-button"

%button--modal {
  // ...
}

.modal-button {
  @extend %button--modal;
}

// If you want to...
.modal > .button {
  @extend %button--modal;
}

%button--modal = concept of a "modal button"

Perfectly kosher @extends

  • "Silent classes" - like abstract classes
  • "@extend-only selectors"
  • Exact same semantics as classes
  • Ignored in CSS output 🎉
  • Can be dynamically declared
    • %like-#{$this}
    • Try doing that with @mixins
      (spoiler: you can't)

@extend selectors as few times as possible.

%button {
  // ...
}

.button {
  @extend %button;
}

.button--primary {
  @extend %button;
}

.button-like-thing {
  @extend %button;
}

input[type="button"] {
  @extend %button;
}

button {
  @extend %button;
}
  • Causes selector explosion
  • Not good for architecture
  • Unwanted inheritance
  • Unnecessary overrides

@extend selectors as few times as possible.

%button {
  // ...
}

.button {
  @extend %button;
}

.button--primary {
  @extend %button;
}

.button-like-thing {
  @extend %button;
}

input[type="button"] {
  @extend %button;
}

button {
  @extend %button;
}

Instead of this:

ButtonA

ButtonB

ButtonC

Prefer this:

Button

A

B

C

  • Less components, more variants
  • BEM
    .button.button--variant
  • RSCSS
    .my-button.-variant
%button {
  // ...
}

.my-button {
  @extend %button;

  &.-primary {
    background: #C0FFEE;
  }
}
<button class="my-button -primary">
  Primary button!
</button>

<button class="my-button">
  Default button
</button>

Good CSS architecture starts with great HTML architecture.

- Me

  • No more extraneous @extends
  • Cleaner, more predictable HTML
  • Explicit over implicit

@extend selectors as few times as possible.

How to avoid . class-itis?




<button class="my-button my-button--primary my-button--block">

Let's say you want to avoid this.




<button class="my-button my-button--primary">

Let's say you want to avoid this.

And you just want this.

<button class="my-button--primary">

Or this. (RSCSS)

<button class="my-button -primary">
%button {
  // ...
}

// RSCSS
.my-button {
  @extend %button;
}


// BEM
[class^="my-button-"] {
  @extend %button;
}

Use @extend for relationships.

This is probably one of the most important uses of @extend.

Use @extend for relationships.

Child

Parent

.parent > .child

Use @extend for relationships.

Descendant

Ancestor

.ancestor .descendant

Use @extend for relationships.

Grandchild

Parent

.parent > * > .grandchild

Child

Use @extend for relationships.

Non-child descendant

Parent

.parent > * .non-child-descendant

*

*

Use @extend for relationships.

Sibling

foo

.sibling-foo ~ .sibling-bar

Sibling

bar

Use @extend for relationships.

Sibling

.sibling + .adjacent-sibling

Adjacent

Sibling

Use @extend for relationships.

Sibling

.sibling + * ~ .non-adjacent-sibling

Non-

Adjacent

Sibling

Sibling

Use @extend for relationships.

Uncle

.uncle ~ * > .nephew

Nephew

*

Use @extend for relationships.

Define relationships in %placeholders.

.my-modal {
  // ...

  > .my-button {
    // ...
  }
}

.my-button {
  // ...
}
  • .my-button has styles defined in two different places
  • Hard to have flexible selectors
  • Difficult for multiple extensions
  • Easy for limited components
  • Good if you have a strict naming system
  • But you won't
%modal {
  // ...

  > %button {
    // ...
  }
}


.my-modal {
  @extend %modal;
}

.my-button {
  @extend %button;
}
  • modal-button relationship abstracted!
  • Selectors defined only once
  • Easier to extend concepts (placeholders) than real selectors
  • Easier to remove unused CSS
  • Extension preserves relationships

Use @extend inside similar media queries.

Use @extend inside similar media queries.

%button {
  display: inline-block;
  width: auto;
  // ...

  &--block {
    display: block;
    width: 100%;
  }
}

.my-button {
  @extend %button;

  &.-block {
    @extend %button--block;
  }

  @media (max-width: 600px) {
    @extend %button--block;
  }
}

You many not @extend an outer selector from within @media.

You may only @extend selectors within the same directive.

Use @extend inside similar media queries.

%button {
  display: inline-block;
  width: auto;
  // ...

  &--block {
    display: block;
    width: 100%;
  }
}

.my-button {
  @extend %button;

  &.-block {
    @extend %button--block;
  }

  @media (max-width: 600px) {
    @extend %button--block;
  }
}

"Style buttons like this when they're in any media context."

"Style buttons like this when they're in this specific media context."

Conflict of interest. 😖

Use @extend inside similar media queries.

%button {
  display: inline-block;
  width: auto;
  // ...

  &--block {
    display: block;
    width: 100%;
  }
}

.my-button {
  @extend %button;

  &.-block {
    @extend %button--block;
  }

  @media (max-width: 600px) {
    @extend %button--block;
  }
}

.my-button.-block {
  width: 100%;
}

@media (max-width: 600px) {
  .my-button {
    /* ??? */
  }
}

Impossible to "copy selectors" if one's in a media context.

So, what can we do?

Use @extend inside similar media queries.

%button {
  display: inline-block;
  width: auto;
  // ...

  &--block {
    display: block;
    width: 100%;
  }

  @media (max-width: 600px) {
    display: block;
    width: 100%;
  }
}

.my-button {
  @extend %button;

  &.-block {
    @extend %button--block;
  }
}

Define the @media constraint in your placeholders.

Less code in your "exported" classes!

Use a @mixin if you want.

Use @extend inside similar media queries.

%button {
  display: inline-block;
  width: auto;

  &--block {
    display: block;
    width: 100%;
  }

  @media (max-width: 600px) {
    &--mobile {
      display: block;
      width: 100%;
    }
  }
}

.my-button {
  @extend %button;

  &.-block { @extend %button--block; }

  @media (max-width: 600px) {
    @extend %button--mobile;
  }
}

Separate into its own %placeholder.

More explicit, no surprises

Use @extend inside similar media queries.

%button {
  display: inline-block;
  width: auto;

  @media (max-width: 600px) {
    &--mobile {
      display: block;
      width: 100%;
    }
  }
}

.my-button { @extend %button; }

@media (max-width: 600px) {
  .special-mobile-button {
    @at-root (without: media) {
      @extend %button;
    }

    @extend %button--mobile;
  }
}

Break out of @media with

@at-root (without: media)

.special-mobile-button shares base styles with %button, but has its own styles.

Wow, an actual use-case for @at-root!

Use @extend inside similar media queries.

BOTTOM LINE:

Only extend within the same media query.

One day you'll be able to @extend in different media queries.

Maybe.

Use @extend with :matches() in the future.

%section {
  // ...

  %heading {
    font-family: Comic Sans;
  }
}


.my-section {
  @extend %section;
}

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

Looks harmless, but you get selector explosion:

.my-section h1, .my-section h2, .my-section h3,
.my-section h4, .my-section h5, .my-section h6 {
  font-family: Comic Sans;
}

Use @extend with :matches() in the future.

%section {
  // ...

  %heading {
    font-family: Comic Sans;
  }
}


.my-section {
  @extend %section;
}

:matches(h1, h2, h3, h4, h5, h6) {
  @extend %heading;
}

:matches is a simple selector, not a selector list!

.my-section :matches(h1, h2, h3, h4, h5, h6) {
  font-family: Comic Sans;
}
  • Great way to avoid selector explosion.
  • Weird browser support:
    • ::-moz-any
    • ::-webkit-any
    • IE: nope

You should get back with your @extend.

It misses you.

Questions?

Special thanks to:

  • Natalie Weizenbaum - @nex3
  • Hugo Giraudel - @hugogiraudel
  • Chris Eppstein - @chriseppstein
  • Una Kravets - @una

Further reading:

Thanks, everyone! #teamExtend