Understanding @extend
The @extend directive is a powerful feature in Sass that allows one selector to inherit
the styles of another. It provides a way to share properties between selectors without repeating
code, creating a relationship that says "this selector should have all the styles of that selector,
plus its own specific styles."
Analogy: @extend is like Genetic Inheritance
Think of @extend as genetic inheritance in a family. A child inherits traits from their
parents but also has their own unique characteristics. Similarly, when a CSS selector extends another,
it inherits all the style "traits" while maintaining its own identity and potentially adding its own
unique properties.
Basic @extend Syntax
// 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;
}
The compiled CSS would look like 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;
}
Notice how Sass combines the selectors that share the same declarations. This is
more efficient than the traditional approach of using multiple classes in your HTML
(like <div class="message success">), as it keeps your markup cleaner.
How @extend Works
When you use @extend, Sass doesn't just copy the properties from one selector to another.
Instead, it creates a relationship in the selector structure of your CSS. The extending selector is
added to the selector list of the extended selector, grouping them together for shared styles.
Extending Complex Selectors
You can extend more complex selectors, and the entire selector structure is preserved:
.alert p {
font-weight: bold;
font-size: 14px;
}
.warning-alert {
@extend .alert;
background-color: #ffdd99;
}
// Compiled CSS
.alert, .warning-alert {
/* alert styles */
}
.alert p, .warning-alert p {
font-weight: bold;
font-size: 14px;
}
This is particularly useful for component-based styling, where you might have complex nested structures with multiple related elements.
Placeholder Selectors
One of the most powerful ways to use @extend is with placeholder selectors
(also called "silent classes"). These are special selectors that start with %
and only appear in your CSS when they're extended.
// Define a placeholder selector
%message-shared {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
// Use the placeholder
.success {
@extend %message-shared;
border-color: green;
}
.error {
@extend %message-shared;
border-color: red;
}
// Placeholder is never compiled on its own
// if it's not extended
The compiled CSS would look like this:
.success, .error {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
.success {
border-color: green;
}
.error {
border-color: red;
}
Advantages of Placeholder Selectors
- They don't get compiled into CSS unless extended, reducing code size
- They clearly indicate that a selector exists solely for extending
- They help organize reusable patterns without polluting your HTML class namespace
Multiple Extends
A selector can extend multiple other selectors, accumulating all their properties:
%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');
}
This is similar to multiple inheritance in object-oriented programming, allowing you to compose styles from various sources.
Chaining Extends
Extends can be chained, creating an inheritance hierarchy:
%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;
font-size: 18px;
padding: 12px 24px;
}
In the compiled CSS, .primary-large would be included in both
the selector list for %base-button and %primary-button.
Limitations and Pitfalls
The Cascade Problem
When using @extend, you need to be careful about the cascade (the order of CSS rules).
Extended selectors maintain their original position in the CSS, which can sometimes lead to
unexpected results:
// This appears first in your Sass
.alert {
border: 1px solid #ccc;
}
// Later in your Sass
.important {
font-weight: bold;
}
// Even later
.error {
@extend .alert;
border-color: red;
}
// The compiled CSS keeps the original order:
.alert, .error {
border: 1px solid #ccc;
}
.important {
font-weight: bold;
}
.error {
border-color: red;
}
If you had a rule affecting .important and .alert together,
the extension wouldn't necessarily work as expected.
The Media Query Limitation
@extend doesn't work across different @media blocks:
// This works
.base-style {
color: blue;
}
@media (min-width: 768px) {
.responsive-element {
@extend .base-style; // Error! Can't extend across media queries
}
}
To solve this, you'd need to include the .base-style inside the media query
or use a mixin instead.
Selector Explosion
Complex extends can lead to large selector groups, especially when extending nested selectors:
// This seemingly simple code...
.sidebar a {
color: blue;
font-weight: bold;
}
.posts a {
@extend a;
}
// ...could compile to something complex like:
.sidebar a, .sidebar .posts a, .posts .sidebar a {
color: blue;
font-weight: bold;
}
This "selector explosion" can make your CSS larger and harder to debug. Use extends judiciously, especially with nested selectors.
@extend vs. Mixins: When to Use Each
@extend and @mixin both allow you to reuse styles, but they work
differently and are best used for different purposes:
| Feature | @extend | @mixin |
|---|---|---|
| Output CSS | Combines selectors, generates less CSS | Duplicates properties for each use, generates more CSS |
| Parameters | No parameter support | Supports parameters and default values |
| Media Queries | Doesn't work across media queries | Works across media queries |
| Complexity | Can create complex selector relationships | Simpler "copy and paste" behavior |
| Use Case | Sharing properties between related elements | Generating utility styles with variations |
When to Use @extend
- For truly related elements that share a common base style
- When you want to keep your CSS size smaller
- For inheritance relationships (like button variants)
- When you don't need parameters
When to Use a Mixin
- When you need to pass parameters to customize the output
- For vendor prefixing and cross-browser compatibility
- Inside media queries
- When you want to generate entirely new styles rather than relate existing ones
Best Practices for Using @extend
Use Placeholder Selectors
Placeholders make it clear that a style exists only to be extended, and they don't add any unused classes to your CSS output:
// Good practice
%button-base {
// Base styles
}
.primary-button {
@extend %button-base;
// Primary-specific styles
}
// Less ideal
.button-base {
// Base styles
}
.primary-button {
@extend .button-base;
// Primary-specific styles
}
Keep It Simple
Avoid extending complex nested selectors. Instead, extend simple, single-class selectors or placeholders:
// Good practice
%centered {
margin-left: auto;
margin-right: auto;
display: block;
}
.profile-image {
@extend %centered;
width: 100px;
border-radius: 50%;
}
// Avoid this
nav ul li a {
// Complex styles
}
.special-link {
@extend a; // This could create a selector explosion
}
Group Related Styles
Keep your extends organized by grouping related placeholders together:
// Typography placeholders
%heading-base {
font-family: 'Helvetica', sans-serif;
font-weight: bold;
}
%body-text {
font-family: 'Georgia', serif;
line-height: 1.6;
}
// Component placeholders
%card-base {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
%button-base {
display: inline-block;
padding: 8px 16px;
border-radius: 4px;
}
Use Descriptive Names
Name your placeholders based on their purpose, not their appearance:
// Good names (based on purpose)
%alert-base
%button-base
%card-base
%form-input
// Less ideal names (based on appearance)
%blue-box
%rounded-corners
%big-text
Combine with Mixins When Appropriate
Sometimes using both extends and mixins together provides the best solution:
// A base placeholder for all buttons
%button-base {
display: inline-block;
border-radius: 4px;
cursor: pointer;
font-weight: normal;
text-align: center;
vertical-align: middle;
}
// A mixin for customizing buttons
@mixin button-variant($bg-color, $text-color, $border-color) {
background-color: $bg-color;
color: $text-color;
border: 1px solid $border-color;
&:hover {
background-color: darken($bg-color, 10%);
border-color: darken($border-color, 12%);
}
}
// Using both together
.primary-button {
@extend %button-base;
@include button-variant(#0066cc, white, #0059b3);
}
.danger-button {
@extend %button-base;
@include button-variant(#dc3545, white, #bd2130);
}
Real-World Example: Component Library
Let's see how @extend might be used in a real-world component library for a web application:
Form Controls System
// _forms.scss
// Base form control
%form-control {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
&:focus {
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
&:disabled,
&[readonly] {
background-color: #e9ecef;
opacity: 1;
}
}
// Different input types extend the base
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="url"],
input[type="tel"],
input[type="search"] {
@extend %form-control;
}
textarea {
@extend %form-control;
height: auto;
}
select {
@extend %form-control;
padding-right: 1.75rem;
background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right 0.75rem center;
background-size: 8px 10px;
appearance: none;
}
// Form validation states
%form-control-valid {
border-color: #28a745;
&:focus {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
}
%form-control-invalid {
border-color: #dc3545;
&:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
}
// Applied conditionally by JavaScript or server-side validation
.is-valid {
@extend %form-control-valid;
}
.is-invalid {
@extend %form-control-invalid;
}
// Form layouts
%form-inline {
display: flex;
flex-flow: row wrap;
align-items: center;
}
.form-inline {
@extend %form-inline;
.form-control {
display: inline-block;
width: auto;
vertical-align: middle;
}
.form-group {
display: flex;
flex: 0 0 auto;
flex-flow: row wrap;
align-items: center;
margin-bottom: 0;
}
}
Alert Component System
// _alerts.scss
// Base alert styles
%alert {
position: relative;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
display: flex;
align-items: center;
}
// Alert variants
%alert-info {
@extend %alert;
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
%alert-success {
@extend %alert;
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
%alert-warning {
@extend %alert;
color: #856404;
background-color: #fff3cd;
border-color: #ffeeba;
}
%alert-danger {
@extend %alert;
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
// Public alert classes
.alert-info {
@extend %alert-info;
}
.alert-success {
@extend %alert-success;
}
.alert-warning {
@extend %alert-warning;
}
.alert-danger {
@extend %alert-danger;
}
// Additional alert features
%alert-dismissible {
padding-right: 4rem;
.close {
position: absolute;
top: 0;
right: 0;
padding: 1rem;
color: inherit;
}
}
.alert-dismissible {
@extend %alert-dismissible;
}
Practice Activities
- Button Component System: Create a system of button styles using placeholder selectors and @extend. Include base button styles and at least three variants (primary, secondary, danger).
-
Message Component Refactoring: Take the following CSS for a message component and
refactor it using @extend to reduce repetition:
.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; } - Card Component Family: Build a family of card components (basic card, product card, profile card, etc.) using placeholders and @extend to share common styles while adding specific variations.
- Form Element System: Create a comprehensive form element styling system using @extend to maintain consistent styling across different input types and states.
Additional Resources
- @extend Official Documentation
- The @extend Concept on CSS-Tricks
- Extending in Sass Without Mess
- U.S. Web Design System - A real-world project that uses @extend effectively
Key Takeaways
- @extend allows selectors to inherit styles from other selectors, reducing code duplication
- Placeholder selectors (%) create styles that only appear in your CSS when extended
- @extend combines selectors in the output, while mixins copy properties
- @extend has limitations with media queries and complex selector relationships
- Best practices include using placeholders, keeping it simple, and using descriptive names
- For optimal flexibility, consider combining @extend with mixins when appropriate