Inheritance and the @extend Directive

Module 7: CSS Preprocessors & Frameworks - Advanced Sass

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

flowchart TB A[Original Selector] -->|"@extend"| B[Extended Selector] C[Original Properties] --> A B --> D[New Properties] E[Compiled CSS] --> F[Combined Selector Group] E --> G[Individual Overrides] F --> H["Original Selector, Extended Selector"] G --> I["Specific Properties for Each Selector"]

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.

graph TD A["%base-button"] --> B["%primary-button"] B --> C[".primary-large"] A -. "Compiled CSS: %base-button, %primary-button, .primary-large" .-> D[".primary-large inherits from both"]

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

  1. 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).
  2. 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;
    }
  3. 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.
  4. Form Element System: Create a comprehensive form element styling system using @extend to maintain consistent styling across different input types and states.

Additional Resources

Key Takeaways

Next Lecture

In our next session, we'll explore Control Directives and Loops in Sass, which add powerful programming capabilities to your stylesheets.