🎨 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 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