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 @content

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"
  }
}