Extends and Inheritance Patterns
1. %Placeholder Selectors and Silent Classes
| Feature |
Syntax |
Behavior |
Use Case |
| Placeholder Selector |
%placeholder-name |
Define but don't output to CSS |
Shared styles for @extend only |
| Silent Class |
Never appears in compiled CSS |
Only exists when extended |
Avoid unused CSS bloat |
| Extension |
@extend %placeholder; |
Copies selector to placeholder rules |
Share styles without duplication |
| No Output |
Unused placeholders removed |
Tree-shaken automatically |
Clean compiled CSS |
| Naming Convention |
Prefix with % |
Clear distinction from classes |
Intent communication |
| Scope |
Global by default |
Available anywhere in import tree |
Shared utilities |
| Private Placeholders |
%-private (with dash) |
Module-private in modern Sass |
Internal-only styles |
Example: Placeholder selectors in practice
// Define placeholder (not output to CSS)
%button-base {
display: inline-block;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
text-align: center;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
}
}
// Extend the placeholder
.btn-primary {
@extend %button-base;
background: #007bff;
color: white;
}
.btn-secondary {
@extend %button-base;
background: #6c757d;
color: white;
}
// CSS Output:
// .btn-primary, .btn-secondary {
// display: inline-block;
// padding: 10px 20px;
// ...
// }
// .btn-primary:hover, .btn-secondary:hover {
// transform: translateY(-2px);
// }
// .btn-primary { background: #007bff; color: white; }
// .btn-secondary { background: #6c757d; color: white; }
// Card component placeholders
%card-base {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1rem;
}
%card-hover {
&:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transform: translateY(-2px);
}
}
.product-card {
@extend %card-base;
@extend %card-hover;
.title {
font-size: 1.25rem;
font-weight: 600;
}
}
.user-card {
@extend %card-base;
// No hover effect for user cards
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
}
}
// Clearfix placeholder (classic example)
%clearfix {
&::after {
content: "";
display: table;
clear: both;
}
}
.container {
@extend %clearfix;
width: 100%;
}
// Text truncation placeholder
%text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.truncated-title {
@extend %text-truncate;
max-width: 200px;
}
Note: Placeholders are only output if extended. If never used,
they produce zero CSS, making them perfect for library code.
2. @extend Directive and Inheritance Chains
| Concept |
Syntax |
Behavior |
Example |
| Basic Extend |
@extend .class; |
Adds selector to existing rules |
.btn { @extend .base; } |
| Extend Placeholder |
@extend %placeholder; |
Extends silent class |
Preferred method |
| Selector Grouping |
Combines selectors |
DRY compiled output |
.a, .b { rules } |
| Inheritance Chain |
A extends B extends C |
Transitive extension |
Multi-level inheritance |
| Complex Selectors |
Extends compound selectors |
All variations included |
.parent .child extended |
| Context Preservation |
Maintains selector context |
Pseudo-classes/elements preserved |
:hover, ::before |
| !optional Flag |
@extend .class !optional; |
Don't error if not found |
Conditional extension |
Example: @extend directive usage
// Basic extension
.message {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.message-success {
@extend .message;
border-color: green;
background: #d4edda;
}
.message-error {
@extend .message;
border-color: red;
background: #f8d7da;
}
// Compiled CSS:
// .message, .message-success, .message-error {
// padding: 10px;
// border: 1px solid #ccc;
// border-radius: 4px;
// }
// .message-success { border-color: green; background: #d4edda; }
// .message-error { border-color: red; background: #f8d7da; }
// Inheritance chain
%base-button {
display: inline-block;
padding: 8px 16px;
border: none;
cursor: pointer;
}
%primary-button {
@extend %base-button;
background: #007bff;
color: white;
}
.cta-button {
@extend %primary-button; // Gets both %primary-button AND %base-button
font-size: 1.2rem;
font-weight: bold;
}
// Complex selector extension
.error {
color: red;
&:hover {
color: darkred;
}
&::before {
content: '⚠ ';
}
}
.validation-error {
@extend .error; // Gets all .error rules including :hover and ::before
font-size: 0.875rem;
}
// Compiled output includes:
// .error, .validation-error { color: red; }
// .error:hover, .validation-error:hover { color: darkred; }
// .error::before, .validation-error::before { content: '⚠ '; }
// Optional extension (no error if not found)
.special-button {
@extend .button-base !optional; // Won't error if .button-base doesn't exist
background: purple;
}
// Nested context preservation
.sidebar {
.widget {
padding: 1rem;
background: white;
}
}
.dashboard {
.panel {
@extend .widget; // Becomes: .sidebar .panel { ... }
}
}
// Multiple extends in one selector
.alert {
@extend %card-base;
@extend %shadow;
@extend %rounded;
padding: 1rem;
margin: 1rem 0;
}
Warning: Extending complex selectors can create unexpected selector
combinations. Prefer extending placeholders or simple classes.
| Aspect |
@extend |
@include (Mixin) |
Winner |
| CSS Output Size |
Smaller (selector grouping) |
Larger (duplicated rules) |
@extend |
| Gzip Compression |
Less effective (varied selectors) |
More effective (repeated rules) |
@include |
| HTTP/2 Impact |
Minimal difference |
Minimal difference |
Neutral |
| Flexibility |
Static (no parameters) |
Dynamic (accepts arguments) |
@include |
| Predictability |
Complex selector generation |
Predictable output |
@include |
| Debugging |
Harder (grouped selectors) |
Easier (local rules) |
@include |
| Browser Performance |
Slightly faster (fewer rules) |
Negligible difference |
Minimal impact |
| Maintenance |
Fragile (cascading changes) |
Isolated (local changes) |
@include |
| Modern Recommendation |
Use sparingly |
Preferred approach |
@include |
Example: Output size comparison
// Using @extend
%button-base {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.btn-primary { @extend %button-base; background: blue; }
.btn-secondary { @extend %button-base; background: gray; }
.btn-success { @extend %button-base; background: green; }
// CSS Output (Smaller - 8 lines):
// .btn-primary, .btn-secondary, .btn-success {
// padding: 10px 20px;
// border: none;
// border-radius: 4px;
// font-size: 14px;
// cursor: pointer;
// }
// .btn-primary { background: blue; }
// .btn-secondary { background: gray; }
// .btn-success { background: green; }
// Using @include
@mixin button-base {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.btn-primary { @include button-base; background: blue; }
.btn-secondary { @include button-base; background: gray; }
.btn-success { @include button-base; background: green; }
// CSS Output (Larger - 18 lines, but more predictable):
// .btn-primary {
// padding: 10px 20px;
// border: none;
// border-radius: 4px;
// font-size: 14px;
// cursor: pointer;
// background: blue;
// }
// .btn-secondary {
// padding: 10px 20px;
// border: none;
// border-radius: 4px;
// font-size: 14px;
// cursor: pointer;
// background: gray;
// }
// .btn-success {
// padding: 10px 20px;
// border: none;
// border-radius: 4px;
// font-size: 14px;
// cursor: pointer;
// background: green;
// }
// File size comparison (uncompressed):
// @extend: ~250 bytes
// @include: ~450 bytes
// File size comparison (gzipped):
// @extend: ~180 bytes (less compression)
// @include: ~190 bytes (better compression due to repetition)
// Difference: Negligible (~10 bytes)
// Flexibility comparison:
// ❌ @extend - cannot accept parameters
%card {
padding: 1rem;
border-radius: 4px;
}
// ✅ @include - dynamic parameters
@mixin card($padding: 1rem, $radius: 4px) {
padding: $padding;
border-radius: $radius;
}
.small-card { @include card(0.5rem, 2px); }
.large-card { @include card(2rem, 8px); }
When to Use Each
- Use
@extend: Shared static styles, semantic relationships, minimal variations
- Use
@include: Dynamic styles with parameters, predictable output, maintainability priority
- Modern preference:
@include for most cases due to flexibility and maintainability
- Performance difference is negligible in real-world applications
4. Multiple Inheritance and Selector Optimization
| Pattern |
Technique |
Output |
Consideration |
| Multiple Extends |
Multiple @extend in one selector |
Selector added to all extended rules |
Clean and efficient |
| Chained Extension |
A extends B, B extends C |
Transitive inheritance |
Hierarchical relationships |
| Selector Explosion |
Many selectors extending same base |
Long selector lists |
Monitor output size |
| Compound Selectors |
Extending complex selectors |
Multiple combined selectors |
Can be unpredictable |
| Smart Unification |
Sass merges related selectors |
Optimized grouping |
Automatic optimization |
| Specificity Control |
Extends don't add specificity |
Same specificity as extended |
CSS cascade friendly |
Example: Multiple inheritance patterns
// Multiple extends in one selector
%flex-center {
display: flex;
justify-content: center;
align-items: center;
}
%rounded {
border-radius: 8px;
}
%shadow {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.modal {
@extend %flex-center;
@extend %rounded;
@extend %shadow;
padding: 2rem;
background: white;
}
// Chained extension
%base-element {
margin: 0;
padding: 0;
}
%interactive-element {
@extend %base-element;
cursor: pointer;
user-select: none;
}
%button-element {
@extend %interactive-element;
border: none;
background: transparent;
}
.custom-button {
@extend %button-element; // Gets all three: base, interactive, and button
color: blue;
}
// Selector optimization in action
%card {
background: white;
padding: 1rem;
}
.product-card { @extend %card; }
.user-card { @extend %card; }
.info-card { @extend %card; }
.alert-card { @extend %card; }
.promo-card { @extend %card; }
// Compiled output (optimized):
// .product-card, .user-card, .info-card, .alert-card, .promo-card {
// background: white;
// padding: 1rem;
// }
// Smart unification example
.parent {
.child {
color: blue;
}
}
.container {
.item {
@extend .child; // Creates: .parent .container .item { color: blue; }
}
}
// Compound selector extension (caution!)
.nav {
li {
a {
color: blue;
&:hover {
color: darkblue;
}
}
}
}
.sidebar {
.link {
@extend a; // Complex output, multiple selector variations generated
}
}
// Better approach - use placeholder
%link-styles {
color: blue;
&:hover {
color: darkblue;
}
}
.nav li a {
@extend %link-styles;
}
.sidebar .link {
@extend %link-styles;
}
// Compiled: .nav li a, .sidebar .link { color: blue; }
// .nav li a:hover, .sidebar .link:hover { color: darkblue; }
5. @extend Limitations and Anti-patterns
| Limitation |
Issue |
Impact |
Solution |
| Media Query Restriction |
Cannot extend across @media |
Compilation error |
Use mixins instead |
| Directive Boundary |
Cannot extend outside directive |
Scope limitations |
Define placeholders globally |
| No Parameters |
Static only, no variables |
Inflexible |
Use @include for dynamic |
| Selector Explosion |
Too many extends creates huge lists |
Large CSS output |
Limit extends per placeholder |
| Complex Selectors |
Unpredictable output |
Debugging difficulty |
Extend simple selectors only |
| Cascade Issues |
Order-dependent behavior |
Maintenance complexity |
Document dependencies |
| Cannot Extend Itself |
Circular extension error |
Compilation fails |
Review inheritance chain |
Example: Common anti-patterns and solutions
// ❌ ANTI-PATTERN: Extending across media queries
%mobile-text {
font-size: 14px;
}
@media (min-width: 768px) {
.heading {
@extend %mobile-text; // ERROR: Cannot extend outside @media
}
}
// ✅ SOLUTION: Use mixin
@mixin mobile-text {
font-size: 14px;
}
@media (min-width: 768px) {
.heading {
@include mobile-text; // Works!
}
}
// ❌ ANTI-PATTERN: Extending complex nested selectors
.nav {
ul {
li {
a {
color: blue;
}
}
}
}
.footer {
.link {
@extend a; // Generates: .nav ul li .footer .link (confusing!)
}
}
// ✅ SOLUTION: Use placeholder with simple selector
%link-base {
color: blue;
}
.nav ul li a {
@extend %link-base;
}
.footer .link {
@extend %link-base;
}
// ❌ ANTI-PATTERN: Overusing extends (selector explosion)
%button {
padding: 10px;
}
// 50+ selectors extending %button
.btn-1 { @extend %button; }
.btn-2 { @extend %button; }
// ... 50 more
.btn-50 { @extend %button; }
// Output: .btn-1, .btn-2, ..., .btn-50 { padding: 10px; }
// (creates very long selector lists)
// ✅ SOLUTION: Use utility class directly in HTML
.btn { padding: 10px; }
// HTML: <div class="btn btn-1">
// ❌ ANTI-PATTERN: Circular extension
%a {
@extend %b;
}
%b {
@extend %a; // ERROR: Circular extension
}
// ❌ ANTI-PATTERN: Extending from multiple unrelated hierarchies
.component1 {
.inner {
color: red;
}
}
.component2 {
.section {
.inner {
background: blue;
}
}
}
.my-element {
@extend .inner; // Which .inner? Ambiguous!
}
// ✅ SOLUTION: Use explicit placeholders
%red-text {
color: red;
}
%blue-background {
background: blue;
}
.component1 .inner { @extend %red-text; }
.component2 .section .inner { @extend %blue-background; }
.my-element { @extend %red-text; }
// ❌ ANTI-PATTERN: Extending instead of composition
.button {
padding: 10px;
border: none;
}
.primary-button {
@extend .button;
background: blue;
}
.large-primary-button {
@extend .primary-button; // Deep inheritance chain
font-size: 1.5rem;
}
// ✅ SOLUTION: Composition over inheritance
.button { padding: 10px; border: none; }
.button-primary { background: blue; }
.button-large { font-size: 1.5rem; }
// HTML: <button class="button button-primary button-large">
Warning: @extend cannot work across @media boundaries. Use
@include for responsive styles.
6. Modern Alternatives to @extend
| Alternative |
Technique |
Advantages |
Use Case |
| CSS Custom Properties |
CSS variables |
Runtime dynamic, no compilation |
Theming, dynamic values |
| Utility Classes |
Atomic CSS approach |
Reusable in HTML, no Sass needed |
Tailwind-style utilities |
| Mixins with @include |
Parametric reuse |
Flexible, predictable |
Most Sass patterns |
| Composition Pattern |
Multiple classes in HTML |
Explicit, maintainable |
Component-based design |
| CSS @layer |
Cascade layers |
Better cascade control |
Modern CSS architecture |
| PostCSS Plugins |
Build-time optimization |
Advanced processing |
Custom transformations |
| CSS-in-JS |
JavaScript styling |
Component-scoped, dynamic |
React/Vue components |
Example: Modern alternatives in practice
// Alternative 1: CSS Custom Properties (CSS Variables)
:root {
--button-padding: 10px 20px;
--button-border: none;
--button-radius: 4px;
--button-cursor: pointer;
}
.btn-primary {
padding: var(--button-padding);
border: var(--button-border);
border-radius: var(--button-radius);
cursor: var(--button-cursor);
background: var(--primary-color, blue);
}
// Benefits: Runtime dynamic, themeable without recompilation
// Alternative 2: Utility Classes (Tailwind approach)
// Define once, use in HTML
.p-4 { padding: 1rem; }
.rounded { border-radius: 0.25rem; }
.bg-blue-500 { background: #3b82f6; }
.text-white { color: white; }
// HTML: <button class="p-4 rounded bg-blue-500 text-white">
// Alternative 3: Mixins (Recommended for Sass)
@mixin button-base($bg-color, $text-color) {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
background: $bg-color;
color: $text-color;
&:hover {
background: darken($bg-color, 10%);
}
}
.btn-primary { @include button-base(#007bff, white); }
.btn-success { @include button-base(#28a745, white); }
// Alternative 4: Composition Pattern
.btn { padding: 10px 20px; border: none; cursor: pointer; }
.btn-rounded { border-radius: 4px; }
.btn-primary { background: blue; color: white; }
.btn-large { font-size: 1.2rem; padding: 12px 24px; }
// HTML: <button class="btn btn-rounded btn-primary btn-large">
// Alternative 5: CSS @layer (Modern CSS)
@layer base {
.button {
padding: 10px 20px;
border: none;
}
}
@layer components {
.btn-primary {
@extend .button; // Or just add class in HTML
background: blue;
}
}
// Alternative 6: CSS-in-JS (React example - conceptual)
// const Button = styled.button`
// padding: 10px 20px;
// border: none;
// border-radius: 4px;
// background: ${props => props.variant === 'primary' ? 'blue' : 'gray'};
// `;
// Comparison: Shared styles across breakpoints
// ❌ @extend doesn't work
%card {
padding: 1rem;
background: white;
}
@media (min-width: 768px) {
.mobile-card {
@extend %card; // ERROR!
}
}
// ✅ CSS Custom Properties work
:root {
--card-padding: 1rem;
--card-bg: white;
}
.card {
padding: var(--card-padding);
background: var(--card-bg);
}
@media (min-width: 768px) {
.mobile-card {
padding: var(--card-padding);
background: var(--card-bg);
}
}
// ✅ Mixins work
@mixin card-styles {
padding: 1rem;
background: white;
}
.card { @include card-styles; }
@media (min-width: 768px) {
.mobile-card { @include card-styles; }
}
Extends Summary and Recommendations
- Use
%placeholders for shared static styles (clearfix,
text-truncate)
- Prefer
@include over @extend for most use cases
@extend cannot cross @media boundaries - use mixins instead
- Avoid extending complex nested selectors - use simple placeholders
- Modern CSS: Consider CSS custom properties for theming
- Composition pattern (multiple classes) often better than inheritance
- Monitor compiled CSS size when using many extends
- Document inheritance chains for maintainability
Note: The Sass team recommends using @extend sparingly. Mixins with @include provide better flexibility and are
easier to maintain.