@extend: the ex
David Khourshid, Counsyl
@davidkpiano
About me
- Front-end Engineer @ Counsyl
- Pianist
-
SassholeSass lover - Open-source: github.com/davidkpiano
- Codepen: codepen.io/davidkpiano
- Twitter: (you guessed it) twitter.com/davidkpiano
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
Extend: the Ex
By David Khourshid
Extend: the Ex
- 6,592