Selector Inheritance (@extend)
|
This section documents Sass/SCSS as implemented by Dart Sass, the current official and actively maintained compiler — it is not tied to any specific book, build tool, or CSS framework (Bootstrap, Bulma, etc.). Unlike the other reference sections on this site, no single reference book underpins it: the content was generated with the assistance of AI from general knowledge of Dart Sass, and should be verified against the current official documentation at sass-lang.com before relying on it in production. Sass continues to evolve, so behaviour described here may lag the compiler you are actually running. This section’s bibliography lists the reference material consulted while preparing these pages. |
@extend tells Sass that one selector should inherit another’s styles. Rather than copying the declarations,
Sass rewrites the selector list of the inherited rule so it also matches the extending selector.
Basic usage
.message {
padding: 12px 16px;
border: 1px solid;
border-radius: 4px;
}
.message--success {
@extend .message;
border-color: #43a047;
color: #1b5e20;
}
.message--error {
@extend .message;
border-color: #e53935;
color: #b71c1c;
}
.message, .message--success, .message--error {
padding: 12px 16px;
border: 1px solid;
border-radius: 4px;
}
.message--success { border-color: #43a047; color: #1b5e20; }
.message--error { border-color: #e53935; color: #b71c1c; }
The shared declarations appear once, with all three selectors attached. This is the defining property of
@extend and the reason people reach for it: minimal output, no duplication.
Note what this means in the HTML: class="message—success" alone is now enough, because the compiled CSS
matches it directly. There is no need to also write class="message message—success".
@extend also propagates through every rule the target appears in, including nested and compound selectors:
.message { padding: 12px; }
.panel .message { margin: 8px; }
.message:hover { opacity: 0.9; }
.alert { @extend .message; }
.message, .alert { padding: 12px; }
.panel .message, .panel .alert { margin: 8px; }
.message:hover, .alert:hover { opacity: 0.9; }
That reach is @extend’s power and its main hazard — one `@extend can rewrite many rules across the whole
stylesheet, including ones written in a completely different file.
Placeholder selectors
A placeholder selector starts with %. It behaves like a class for extension purposes but is never emitted
to the output on its own:
%message-base {
padding: 12px 16px;
border: 1px solid;
border-radius: 4px;
}
.message--success {
@extend %message-base;
border-color: #43a047;
}
.message--error {
@extend %message-base;
border-color: #e53935;
}
.message--success, .message--error {
padding: 12px 16px;
border: 1px solid;
border-radius: 4px;
}
.message--success { border-color: #43a047; }
.message--error { border-color: #e53935; }
No %message-base rule is emitted, and no .message class is left in the CSS that nothing uses.
Placeholders are the recommended @extend target. Extending a real class emits that class whether or not
any markup uses it, and couples your rule to a selector that someone may later restyle or delete. A placeholder
exists purely as an extension point, which makes the intent explicit. A placeholder that is never extended
produces no output at all, so unused ones cost nothing.
@extend vs. mixins
Both share styles between rules; they differ in how the sharing reaches the output.
@extend |
Mixin (@include) |
|
|---|---|---|
Mechanism |
Merges selectors onto one rule |
Copies declarations into each caller |
Output size (uncompressed) |
Smaller — declarations appear once |
Larger — declarations repeated per caller |
Output size (after gzip/brotli) |
Comparable — repetition compresses very well |
Comparable |
Accepts arguments |
No |
Yes |
Accepts a content block |
No |
Yes, via |
Source order / cascade |
Rule lands where the target was defined |
Declarations land where the caller is |
Works inside a media query |
Only within the same media context |
Yes, anywhere |
Predictability |
Action at a distance — rewrites distant rules |
Local and obvious |
Selector bloat risk |
High |
None |
Which to use. Default to a mixin. The output-size argument that historically favoured @extend is
largely neutralised by gzip/brotli, which compress repeated declaration blocks extremely well, while mixins
remain local, predictable, and parameterisable. Reach for @extend (always against a %placeholder) when
several selectors genuinely represent the same thing semantically and you want them to share one rule — and
when the set of extenders is small and lives in the same file or module.
Caveats
Media queries
A rule can only extend a selector defined in the same media context. Sass cannot merge selectors across
@media boundaries, because the resulting rule would have to apply in both contexts at once.
%wide-only { float: left; }
@media (min-width: 768px) {
.sidebar {
@extend %wide-only; // ERROR: "%wide-only" was extended from within a
} // media query, but it was defined outside of one
}
The fix is a mixin, which has no such restriction:
@mixin wide-only { float: left; }
@media (min-width: 768px) {
.sidebar { @include wide-only; }
}
This limitation alone rules @extend out of most responsive component code.
Selector bloat
Because @extend rewrites every rule the target appears in, extending a selector that is itself widely used
(or that is itself the product of other extends) can multiply selectors combinatorially. A rule ending up with
hundreds of comma-separated selectors is a known failure mode, and it can make the compiled CSS larger than
the mixin version it was meant to shrink.
Extending inside a loop is the fastest way to get there:
// Dangerous: every generated class joins the same rule's selector list
@each $name in a, b, c, d, e {
.icon-#{$name} { @extend %icon-base; }
}
Source order and specificity surprises
The merged rule appears where the target was defined, not where the @extend was written. If you extend
something defined early in the stylesheet, the inherited declarations land early too — and can be overridden by
rules you expected them to beat. See Selectors & specificity for
how the cascade resolves this.
Extending compound selectors
Only a simple selector can be extended. @extend .a.b or @extend .parent .child is an error:
.a.b { color: red; }
.c { @extend .a.b; } // ERROR: compound selectors may no longer be extended
Extend the individual pieces instead, or use a mixin.
!optional
Extending a selector that does not exist is an error. !optional downgrades that to silence, which is useful in
a library where a hook may or may not be defined:
.button { @extend %theme-hook !optional; }
Use it sparingly — it also silences the genuine typo case.
@extend and &
@extend composes with nesting, but read the result carefully: inside a nested block, the extending selector is
the full compiled selector, so the merge happens against that, not against the innermost fragment.
%focus-ring { outline: 2px solid #3f51b5; }
.form {
input {
&:focus-visible { @extend %focus-ring; } // extends as ".form input:focus-visible"
}
}