Skip to main content

Course Progress

Loading...

🎨 Using Sass in Theme Development

Write more maintainable and powerful CSS with Sass

Master variables, mixins, nesting, and advanced Sass features for WordPress themes

Learning Objectives

  • Understand Sass/SCSS fundamentals
  • Set up Sass compilation in WordPress themes
  • Use variables for consistent theming
  • Create and use mixins for reusable code
  • Organize styles with partials and imports
  • Master nesting and parent selectors
  • Work with functions and operators
  • Implement build workflows for Sass

What is Sass?

Sass (Syntactically Awesome Style Sheets) is a CSS preprocessor that adds powerful features to CSS, making it easier to write and maintain complex stylesheets. Sass compiles to regular CSS that browsers can understand.

💡
Key Concept
Sass extends CSS with features like variables, nesting, mixins, and functions - turning CSS into a more powerful and maintainable language.

Sass vs SCSS

Sass has two syntaxes:

  • SCSS (Sassy CSS): Uses brackets and semicolons like CSS (.scss files)
  • Indented Syntax: Uses indentation instead of brackets (.sass files)

We'll use SCSS as it's more popular and CSS-compatible.

Core Sass Features

Variables

Store reusable values like colors, fonts, and sizes

Nesting

Nest selectors to match HTML structure

Partials

Split CSS into smaller, maintainable files

Mixins

Create reusable groups of CSS declarations

Inheritance

Share properties between selectors

Functions

Use built-in or custom functions for calculations

Setting Up Sass in WordPress Themes

Installation Methods

Method 1: Using NPM and Sass CLI

# Initialize npm in your theme directory
npm init -y

# Install Sass
npm install sass --save-dev

# Add scripts to package.json
"scripts": {
    "sass": "sass src/scss:assets/css --watch",
    "sass:build": "sass src/scss:assets/css --style compressed"
}

Method 2: Using Webpack

# Install webpack and loaders
npm install webpack webpack-cli sass sass-loader css-loader mini-css-extract-plugin --save-dev

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');

module.exports = {
    entry: './src/scss/main.scss',
    output: {
        path: path.resolve(__dirname, 'assets'),
    },
    module: {
        rules: [
            {
                test: /\.scss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].css'
        })
    ]
};

Sass File Organization

Theme Sass Structure

src/scss/
├── main.scss // Main entry point
├── abstracts/
│   ├── _variables.scss // Variables
│   ├── _functions.scss // Functions
│   ├── _mixins.scss // Mixins
│   └── _placeholders.scss // Placeholders
├── base/
│   ├── _reset.scss // CSS reset
│   ├── _typography.scss // Typography
│   └── _base.scss // Base styles
├── components/
│   ├── _buttons.scss // Button styles
│   ├── _cards.scss // Card components
│   ├── _forms.scss // Form styles
│   └── _modals.scss // Modal styles
├── layout/
│   ├── _header.scss // Header
│   ├── _footer.scss // Footer
│   ├── _sidebar.scss // Sidebar
│   └── _grid.scss // Grid system
├── pages/
│   ├── _home.scss // Homepage styles
│   ├── _blog.scss // Blog styles
│   └── _contact.scss // Contact page
├── themes/
│   ├── _default.scss // Default theme
│   └── _dark.scss // Dark theme
├── vendors/
│   └── _bootstrap.scss // Third-party
└── wordpress/
    ├── _blocks.scss // Block styles
    ├── _widgets.scss // Widget styles
    └── _admin.scss // Admin styles

Working with Variables

_variables.scss

// Color Variables
$primary-color: #667eea;
$secondary-color: #764ba2;
$success-color: #48bb78;
$danger-color: #f56565;
$warning-color: #ed8936;
$info-color: #4299e1;

// Neutral Colors
$gray-100: #f7fafc;
$gray-200: #edf2f7;
$gray-300: #e2e8f0;
$gray-400: #cbd5e0;
$gray-500: #a0aec0;
$gray-600: #718096;
$gray-700: #4a5568;
$gray-800: #2d3748;
$gray-900: #1a202c;

// Typography
$font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
$font-secondary: 'Merriweather', Georgia, serif;
$font-mono: 'Fira Code', Monaco, monospace;

$font-size-base: 1rem;
$font-size-sm: 0.875rem;
$font-size-lg: 1.125rem;
$font-size-xl: 1.25rem;

$line-height-base: 1.6;
$line-height-heading: 1.2;

// Spacing
$spacer: 1rem;
$spacers: (
    0: 0,
    1: $spacer * 0.25,  // 4px
    2: $spacer * 0.5,   // 8px
    3: $spacer,         // 16px
    4: $spacer * 1.5,   // 24px
    5: $spacer * 2,     // 32px
    6: $spacer * 3,     // 48px
    7: $spacer * 4,     // 64px
    8: $spacer * 5,     // 80px
);

// Breakpoints
$breakpoints: (
    xs: 0,
    sm: 576px,
    md: 768px,
    lg: 992px,
    xl: 1200px,
    xxl: 1400px
);

// Container widths
$container-max-widths: (
    sm: 540px,
    md: 720px,
    lg: 960px,
    xl: 1140px,
    xxl: 1320px
);

// Border
$border-width: 1px;
$border-color: $gray-300;
$border-radius: 0.375rem;
$border-radius-sm: 0.25rem;
$border-radius-lg: 0.5rem;
$border-radius-full: 9999px;

// Shadows
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
$shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);

// Transitions
$transition-base: all 0.3s ease;
$transition-fade: opacity 0.15s linear;
$transition-collapse: height 0.35s ease;

Using Variables

// Using variables in styles
.button {
    background-color: $primary-color;
    color: white;
    font-family: $font-primary;
    font-size: $font-size-base;
    padding: map-get($spacers, 2) map-get($spacers, 4);
    border-radius: $border-radius;
    transition: $transition-base;
    
    &:hover {
        background-color: darken($primary-color, 10%);
        box-shadow: $shadow-md;
    }
}

Creating and Using Mixins

_mixins.scss

// Breakpoint mixin
@mixin breakpoint($point) {
    @if map-has-key($breakpoints, $point) {
        @media (min-width: map-get($breakpoints, $point)) {
            @content;
        }
    } @else {
        @warn "Breakpoint '#{$point}' not found in $breakpoints map.";
    }
}

// Flexbox center
@mixin flex-center {
    display: flex;
    justify-content: center;
    align-items: center;
}

// Button variant
@mixin button-variant($bg-color, $text-color: white) {
    background-color: $bg-color;
    color: $text-color;
    border: 2px solid $bg-color;
    
    &:hover {
        background-color: darken($bg-color, 10%);
        border-color: darken($bg-color, 10%);
    }
    
    &:focus {
        outline: none;
        box-shadow: 0 0 0 3px rgba($bg-color, 0.5);
    }
    
    &:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }
}

// Typography
@mixin heading($level: 1) {
    font-family: $font-secondary;
    font-weight: 700;
    line-height: $line-height-heading;
    margin-bottom: map-get($spacers, 3);
    
    @if $level == 1 {
        font-size: 2.5rem;
        @include breakpoint(md) {
            font-size: 3rem;
        }
    } @else if $level == 2 {
        font-size: 2rem;
        @include breakpoint(md) {
            font-size: 2.5rem;
        }
    } @else if $level == 3 {
        font-size: 1.75rem;
        @include breakpoint(md) {
            font-size: 2rem;
        }
    }
}

// Clearfix
@mixin clearfix {
    &::after {
        content: "";
        display: table;
        clear: both;
    }
}

// Truncate text
@mixin truncate($width: 100%) {
    max-width: $width;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

// Aspect ratio
@mixin aspect-ratio($width, $height) {
    position: relative;
    
    &::before {
        content: "";
        display: block;
        padding-top: percentage($height / $width);
    }
    
    > * {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
    }
}

// Gradient
@mixin gradient($start-color, $end-color, $angle: 135deg) {
    background: $start-color;
    background: linear-gradient($angle, $start-color 0%, $end-color 100%);
}

// Card
@mixin card($padding: map-get($spacers, 4)) {
    background: white;
    border-radius: $border-radius-lg;
    box-shadow: $shadow;
    padding: $padding;
    transition: $transition-base;
    
    &:hover {
        box-shadow: $shadow-lg;
        transform: translateY(-2px);
    }
}

Using Mixins

// Using mixins in components
.hero-section {
    @include gradient($primary-color, $secondary-color);
    @include flex-center;
    min-height: 500px;
    
    @include breakpoint(md) {
        min-height: 600px;
    }
    
    @include breakpoint(lg) {
        min-height: 700px;
    }
}

h1 {
    @include heading(1);
}

.btn-primary {
    @include button-variant($primary-color);
}

.btn-success {
    @include button-variant($success-color);
}

.card {
    @include card;
    
    &__image {
        @include aspect-ratio(16, 9);
        margin: -#{map-get($spacers, 4)};
        margin-bottom: map-get($spacers, 3);
    }
    
    &__title {
        @include truncate;
        @include heading(3);
    }
}

Nesting and Parent Selectors

Sass Nesting

// Navigation nesting example
.site-navigation {
    background: white;
    box-shadow: $shadow;
    
    .nav-menu {
        display: flex;
        list-style: none;
        margin: 0;
        padding: 0;
        
        li {
            position: relative;
            
            a {
                display: block;
                padding: map-get($spacers, 3) map-get($spacers, 4);
                color: $gray-700;
                text-decoration: none;
                transition: $transition-base;
                
                &:hover {
                    color: $primary-color;
                    background: $gray-100;
                }
                
                &.active {
                    color: $primary-color;
                    font-weight: 600;
                }
            }
            
            // Dropdown menu
            &.has-dropdown {
                > a {
                    &::after {
                        content: "▼";
                        margin-left: 0.5rem;
                        font-size: 0.75em;
                    }
                }
                
                .dropdown-menu {
                    display: none;
                    position: absolute;
                    top: 100%;
                    left: 0;
                    background: white;
                    box-shadow: $shadow-lg;
                    min-width: 200px;
                    
                    a {
                        padding: map-get($spacers, 2) map-get($spacers, 3);
                        
                        &:hover {
                            background: $primary-color;
                            color: white;
                        }
                    }
                }
                
                &:hover .dropdown-menu {
                    display: block;
                }
            }
        }
    }
    
    // Mobile menu
    @include breakpoint(md) {
        .mobile-toggle {
            display: none;
        }
    }
}
Avoid nesting more than 3-4 levels deep as it creates overly specific selectors and makes CSS harder to maintain.

Sass Functions

Custom Functions

// _functions.scss

// Convert px to rem
@function rem($pixels, $base: 16) {
    @return ($pixels / $base) * 1rem;
}

// Convert px to em
@function em($pixels, $base: 16) {
    @return ($pixels / $base) * 1em;
}

// Get z-index value
$z-indexes: (
    dropdown: 1000,
    sticky: 1020,
    fixed: 1030,
    modal-backdrop: 1040,
    modal: 1050,
    popover: 1060,
    tooltip: 1070
);

@function z($layer) {
    @if not map-has-key($z-indexes, $layer) {
        @warn "No z-index found for `#{$layer}`. Available layers: #{map-keys($z-indexes)}";
    }
    @return map-get($z-indexes, $layer);
}

// Color functions
@function tint($color, $percentage) {
    @return mix(white, $color, $percentage);
}

@function shade($color, $percentage) {
    @return mix(black, $color, $percentage);
}

// Strip unit
@function strip-unit($number) {
    @if type-of($number) == 'number' and not unitless($number) {
        @return $number / ($number * 0 + 1);
    }
    @return $number;
}

// Usage examples
.example {
    padding: rem(24);                    // 1.5rem
    font-size: em(18);                   // 1.125em
    z-index: z(modal);                   // 1050
    background: tint($primary-color, 20%); // Lighter version
    border-color: shade($primary-color, 20%); // Darker version
}

Placeholders and @extend

Using Placeholders

// _placeholders.scss

// Button base
%button-base {
    display: inline-block;
    padding: map-get($spacers, 2) map-get($spacers, 4);
    font-family: $font-primary;
    font-size: $font-size-base;
    font-weight: 500;
    line-height: 1.5;
    text-align: center;
    text-decoration: none;
    vertical-align: middle;
    cursor: pointer;
    user-select: none;
    border: 2px solid transparent;
    border-radius: $border-radius;
    transition: $transition-base;
    
    &:hover {
        transform: translateY(-2px);
    }
    
    &:focus {
        outline: none;
    }
    
    &:disabled {
        opacity: 0.65;
        cursor: not-allowed;
    }
}

// Visually hidden
%visually-hidden {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
}

// Container
%container {
    width: 100%;
    padding-right: map-get($spacers, 3);
    padding-left: map-get($spacers, 3);
    margin-right: auto;
    margin-left: auto;
    
    @each $breakpoint, $max-width in $container-max-widths {
        @include breakpoint($breakpoint) {
            max-width: $max-width;
        }
    }
}

// Using placeholders
.btn {
    @extend %button-base;
}

.sr-only {
    @extend %visually-hidden;
}

.container {
    @extend %container;
}

Control Directives

@each, @for, and @if

// Generate utility classes with @each
@each $name, $color in (
    primary: $primary-color,
    secondary: $secondary-color,
    success: $success-color,
    danger: $danger-color,
    warning: $warning-color,
    info: $info-color
) {
    .text-#{$name} {
        color: $color;
    }
    
    .bg-#{$name} {
        background-color: $color;
    }
    
    .border-#{$name} {
        border-color: $color;
    }
    
    .btn-#{$name} {
        @include button-variant($color);
    }
}

// Generate spacing utilities with @for
@for $i from 0 through 8 {
    .m-#{$i} {
        margin: map-get($spacers, $i) !important;
    }
    
    .p-#{$i} {
        padding: map-get($spacers, $i) !important;
    }
    
    @each $side, $property in (
        t: top,
        r: right,
        b: bottom,
        l: left
    ) {
        .m#{$side}-#{$i} {
            margin-#{$property}: map-get($spacers, $i) !important;
        }
        
        .p#{$side}-#{$i} {
            padding-#{$property}: map-get($spacers, $i) !important;
        }
    }
}

// Conditional styles with @if
@mixin theme-colors($theme: 'light') {
    @if $theme == 'dark' {
        background: $gray-900;
        color: $gray-100;
        
        a {
            color: tint($primary-color, 20%);
        }
    } @else if $theme == 'light' {
        background: white;
        color: $gray-900;
        
        a {
            color: $primary-color;
        }
    } @else {
        @warn "Unknown theme: #{$theme}";
    }
}

WordPress-Specific Sass Patterns

WordPress Block Styles

// _blocks.scss
.wp-block {
    &-button {
        @extend %button-base;
        
        &__link {
            @include button-variant($primary-color);
        }
        
        &.is-style-outline {
            .wp-block-button__link {
                background: transparent;
                color: $primary-color;
                border-color: $primary-color;
                
                &:hover {
                    background: $primary-color;
                    color: white;
                }
            }
        }
    }
    
    &-columns {
        margin-bottom: map-get($spacers, 5);
        
        @include breakpoint(md) {
            display: flex;
            gap: map-get($spacers, 4);
        }
        
        .wp-block-column {
            @include breakpoint(md) {
                flex: 1;
            }
        }
    }
    
    &-gallery {
        @include clearfix;
        
        .blocks-gallery-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: map-get($spacers, 3);
        }
    }
}

// Admin bar adjustment
.admin-bar {
    .site-header {
        &.is-sticky {
            top: 32px;
            
            @media screen and (max-width: 782px) {
                top: 46px;
            }
        }
    }
}

Best Practices

Sass Best Practices

  • Use variables consistently: Define all colors, sizes, and values as variables
  • Keep nesting shallow: Maximum 3-4 levels deep
  • Create semantic mixins: Name mixins based on what they do, not how
  • Organize with partials: One component per file
  • Comment your code: Especially complex mixins and functions
  • Use maps for related values: Group related variables in maps
  • Avoid @extend with selectors: Use placeholders instead
  • Compile for production: Always minify for production
  • Lint your Sass: Use stylelint with Sass rules
  • Keep specificity low: Use BEM or similar methodology
Use source maps during development to see the original Sass file and line number in browser DevTools when debugging styles.

Practice Exercise

💻
Convert Theme CSS to Sass

Refactor your theme's CSS using Sass:

  1. Set up Sass compilation in your theme
  2. Create variables for all colors and sizes
  3. Build a mixin library for common patterns
  4. Organize styles into partials
  5. Create responsive breakpoint mixins
  6. Build utility classes with @each loops
  7. Implement theme color variations
  8. Create reusable component styles
  9. Set up source maps for debugging
  10. Configure production build with minification

Additional Resources