SCSS Extends and Inheritance (@extend, %placeholder)
1. %Placeholder Selectors (%name) 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.
3. @extend vs @include - Performance and Output Comparison
| 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:
@includefor most cases due to flexibility and maintainability - Performance difference is negligible in real-world applications
4. Multiple Inheritance (@extend %class1, %class2) Patterns
| 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 (@mixin, CSS custom properties)
| 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
%placeholdersfor shared static styles (clearfix, text-truncate) - Prefer
@includeover@extendfor most use cases @extendcannot cross@mediaboundaries - 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.