🧬 Inheritance and the @extend Directive
Duplication is the enemy of a maintainable stylesheet. Sass's @extend directive lets one selector inherit another's styles — not by copying declarations, but by weaving the selectors together so shared rules appear only once in your output. This lesson shows you how it works, when to reach for a placeholder instead of a class, and the traps that make experienced developers reach for a mixin instead.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain how
@extendgroups selectors in the compiled CSS instead of copying declarations - Use placeholder selectors (
%name) to define reusable styles that emit no CSS until extended - Chain and combine multiple extends to compose styles like an inheritance hierarchy
- Diagnose the three classic pitfalls — the cascade problem, the media-query limitation, and selector explosion
- Decide correctly between
@extendand a@mixinfor a given reuse problem
Estimated Time: 30–40 minutes • Difficulty: Intermediate
Hands-on: Refactor a repetitive four-variant message component down to a single placeholder.
In This Lesson
What @extend Does
The @extend directive tells Sass that "this selector should have all the styles of that selector, plus its own." It is Sass's answer to a very common problem: several elements that are almost the same, differing only in a color here or a border there.
💡 Analogy — genetic inheritance. A child inherits traits from a parent but also has features all their own. When one selector extends another, it inherits every style "trait" of the base while keeping its own identity and adding its own unique properties. The base and its descendants stay related, but each remains distinct.
Here is the canonical example: a base .message box and three variants that share its shape but change the accent color.
// Define a base style
.message {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
color: #333;
}
// Extend the base style
.success {
@extend .message;
border-color: green;
color: green;
}
.error {
@extend .message;
border-color: red;
color: red;
}
.warning {
@extend .message;
border-color: orange;
color: darkorange;
}
Without @extend, you would either repeat the four shared declarations in every rule, or stack classes in your markup like <div class="message success">. The extend approach keeps your HTML clean — a single class="success" — while Sass handles the sharing at compile time.
How It Compiles: Grouping, Not Copying
This is the single most important idea in the lesson: @extend does not copy properties. Instead, it adds the extending selector to the selector list of the extended rule, so the shared declarations are written exactly once.
@extend tends to produce smaller CSS than a mixin.The example from the previous section compiles to this:
.message, .success, .error, .warning {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
color: #333;
}
.success { border-color: green; color: green; }
.error { border-color: red; color: red; }
.warning { border-color: orange; color: darkorange; }
Because it works on the selector structure, @extend preserves complex selectors too. Extending .alert also carries along any descendant rules like .alert p:
.alert p {
font-weight: bold;
font-size: 14px;
}
.warning-alert {
@extend .alert;
background-color: #ffdd99;
}
// Compiled: the descendant rule is preserved for the extender too
// .alert p, .warning-alert p { font-weight: bold; font-size: 14px; }
📖 Key Terms
Extended selector: the base whose styles are being inherited (the source).
Extending selector: the selector that says @extend (the target that gains the styles).
Placeholder selector: a selector starting with % that produces no CSS on its own — only when something extends it.
Placeholder Selectors
Extending a real class like .message has a downside: .message itself is now in your compiled CSS whether or not you ever use it in HTML. Placeholder selectors — also called "silent classes" — solve this. A placeholder starts with % and emits nothing unless it is extended.
// Define a placeholder — silent until extended
%message-shared {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
.success {
@extend %message-shared;
border-color: green;
}
.error {
@extend %message-shared;
border-color: red;
}
Compiles to clean output with no stray .message-shared class:
.success, .error {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
.success { border-color: green; }
.error { border-color: red; }
✅ Why placeholders are the preferred base for @extend
- They produce zero output until extended, so no dead classes bloat your CSS.
- The
%prefix signals intent: this selector exists only to be extended. - They keep your HTML class namespace clean — placeholders can't be used in markup by mistake.
Multiple & Chained Extends
Extending more than one base
A single selector can extend several placeholders, accumulating all their styles — conceptually like multiple inheritance:
%message-base {
padding: 10px;
border-radius: 4px;
}
%with-icon {
padding-left: 30px;
background-repeat: no-repeat;
background-position: 10px center;
}
.info-message {
@extend %message-base;
@extend %with-icon;
background-color: #e6f7ff;
border: 1px solid #91d5ff;
background-image: url("info-icon.svg");
}
Chaining extends into a hierarchy
Extends can chain: a placeholder can extend another placeholder, forming an inheritance tree. The final selector is folded into every ancestor's grouped rule.
%base-button {
display: inline-block;
padding: 8px 16px;
border-radius: 4px;
text-align: center;
cursor: pointer;
}
%primary-button {
@extend %base-button;
background-color: #0066cc;
color: white;
}
.primary-large {
@extend %primary-button; // gains %primary-button AND %base-button
font-size: 18px;
padding: 12px 24px;
}
In the output, .primary-large appears in the selector list for both %base-button and %primary-button. That is inheritance composed at compile time, with no runtime cost.
Limitations and Pitfalls
@extend is powerful, but it manipulates the selector layer of your CSS, and that has consequences. Three traps catch people repeatedly.
1. The cascade problem
Extended selectors keep their original source position. Because CSS resolves ties by source order, an extend can land your grouped rule in a place you did not expect, so a later rule you thought would win may not.
.alert { border: 1px solid #ccc; } // appears early
.important{ font-weight: bold; }
.error { @extend .alert; border-color: red; } // appears late
// Compiled — .alert group stays at the EARLY position:
// .alert, .error { border: 1px solid #ccc; }
// .important { font-weight: bold; }
// .error { border-color: red; }
2. @extend can't cross a @media boundary
You cannot extend a selector that lives outside the current @media block. This is a hard error, not a warning.
.base-style { color: blue; }
@media (min-width: 768px) {
.responsive-element {
@extend .base-style; // ❌ Error: can't extend across media queries
}
}
⚠️ The fix is almost always a mixin
Anything you need to reuse inside media queries should be a @mixin, because @include copies declarations into the current context and works anywhere. Reach for @extend only for context-free, top-level shared styles.
3. Selector explosion
Extending a bare element or a nested selector can multiply combinations combinatorially, producing huge, hard-to-debug selector lists:
.sidebar a { color: blue; font-weight: bold; }
.posts a { @extend a; }
// ...can compile to something like:
// .sidebar a, .sidebar .posts a, .posts .sidebar a { color: blue; font-weight: bold; }
The rule of thumb: only ever extend a simple, single placeholder or class — never a bare tag and never a deeply nested selector.
@extend vs. @mixin
Both reuse styles, but they solve different problems. The quick mental model: @extend relates selectors; @mixin stamps out declarations.
| Feature | @extend | @mixin |
|---|---|---|
| Output | Groups selectors — usually less CSS | Duplicates declarations per use — more CSS |
| Parameters | None | Yes, with defaults |
| Inside media queries | Not allowed | Works everywhere |
| Effect on cascade | Can create surprising selector relationships | Predictable "copy where called" |
| Best for | Truly related elements sharing a base | Parameterized/utility styles & anything in media queries |
💡 Rule of thumb
Use @extend when the relationship is "these things are the same kind of thing." Use a @mixin when you need parameters, when you're inside a media query, or when copying is genuinely what you want. When in doubt on a large team, many style guides default to mixins because their output is more predictable.
Worked Example: A Component Library
Let's see a realistic alert system built the way a real design system would. A silent base placeholder holds the shape; per-variant placeholders add color; and public classes expose them. Notice how modern Dart Sass uses the color module (via @use) instead of the deprecated global darken() function.
// _alerts.scss
@use "sass:color";
// Base shape — emits nothing on its own
%alert {
position: relative;
display: flex;
align-items: center;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
}
// A mixin handles the parameterized color work
@mixin alert-variant($text, $bg, $border) {
color: $text;
background-color: $bg;
border-color: $border;
}
// Public classes: extend the shared shape, include the colors
.alert-info { @extend %alert; @include alert-variant(#0c5460, #d1ecf1, #bee5eb); }
.alert-success { @extend %alert; @include alert-variant(#155724, #d4edda, #c3e6cb); }
.alert-warning { @extend %alert; @include alert-variant(#856404, #fff3cd, #ffeeba); }
.alert-danger { @extend %alert; @include alert-variant(#721c24, #f8d7da, #f5c6cb); }
This is the pattern to internalize: the placeholder handles the shared, static shape (via @extend); the mixin handles the per-variant, parameterized color (via @include). Each tool does the job it's best at, and the compiled CSS stays lean — the border-radius, padding, and flex layout are written only once.
Compiled CSS (abridged)
.alert-info, .alert-success, .alert-warning, .alert-danger {
position: relative; display: flex; align-items: center;
padding: 1rem; margin-bottom: 1rem;
border: 1px solid transparent; border-radius: 0.25rem;
}
.alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; }
.alert-success { color: #155724; background-color: #d4edda; border-color: #c3e6cb; }
/* ...and so on */
Hands-on Exercise
🏋️ Refactor a Repetitive Message Component
Objective: Take four near-identical message classes and collapse the shared styles into a single placeholder using @extend.
Here is the repetitive starting CSS. Every rule repeats padding, border-radius, and margin-bottom:
.info-message {
padding: 15px; border-radius: 4px; margin-bottom: 20px;
background-color: #e6f7ff; border: 1px solid #91d5ff; color: #0066cc;
}
.success-message {
padding: 15px; border-radius: 4px; margin-bottom: 20px;
background-color: #e6ffee; border: 1px solid #8eedac; color: #00a854;
}
.warning-message {
padding: 15px; border-radius: 4px; margin-bottom: 20px;
background-color: #fffbe6; border: 1px solid #ffe58f; color: #d48806;
}
.error-message {
padding: 15px; border-radius: 4px; margin-bottom: 20px;
background-color: #fff1f0; border: 1px solid #ffa39e; color: #cf1322;
}
Instructions:
- Create a placeholder
%message-baseholding the three shared declarations. - Rewrite each
*-messageclass to@extend %message-baseand set only its ownbackground-color,border, andcolor. - Predict the grouped selector Sass will produce for the shared rule.
💡 Hint
The shared declarations are exactly padding: 15px; border-radius: 4px; margin-bottom: 20px;. Everything that differs (background, border, text color) stays inside each individual class.
✅ Solution
%message-base {
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.info-message { @extend %message-base; background-color: #e6f7ff; border: 1px solid #91d5ff; color: #0066cc; }
.success-message { @extend %message-base; background-color: #e6ffee; border: 1px solid #8eedac; color: #00a854; }
.warning-message { @extend %message-base; background-color: #fffbe6; border: 1px solid #ffe58f; color: #d48806; }
.error-message { @extend %message-base; background-color: #fff1f0; border: 1px solid #ffa39e; color: #cf1322; }
Sass groups the shared rule as:
.info-message, .success-message, .warning-message, .error-message {
padding: 15px; border-radius: 4px; margin-bottom: 20px;
}
Twelve repeated lines became three — and there's no stray .message-base class in the output.
🎯 Quick Quiz
Question 1: What does @extend actually do to the compiled CSS?
Question 2: Why are placeholder selectors (%name) usually preferred as the base for an extend?
Question 3: You need to reuse a block of styles inside several @media queries, and it needs a parameter. Which tool fits?
Summary & Quiz
🎉 Key Takeaways
@extendgroups selectors in the output rather than copying declarations, so shared styles are written once.- Placeholder selectors (
%name) emit no CSS until extended — the cleanest base for an extend. - Extends can be chained and combined to compose an inheritance hierarchy at compile time.
- Watch the three pitfalls: the cascade problem, the no-cross-media-query rule, and selector explosion.
- Prefer
@extendfor related elements sharing a base; prefer a mixin for parameters and anything inside media queries.
📚 Further Reading
- Sass docs — the @extend directive
- CSS-Tricks — The @extend Concept
- Smashing Magazine — Extending in Sass Without Mess
🚀 What's Next?
You've learned how Sass reuses styles by relating selectors. Next we add real programming logic to stylesheets — conditionals and loops — in Control Directives and Loops, where @if, @for, @each, and @while let you generate whole systems of classes from a few lines of Sass.
🎉 Nicely done!
You can now cut duplication with confidence — and you know exactly when to leave it to a mixin instead.