Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

SCSS

A comprehensive guide to SCSS (Sassy CSS), a CSS preprocessor that adds powerful features like variables, nesting, mixins, and functions to standard CSS.

SCSS

A comprehensive guide to SCSS (Sassy CSS), a CSS preprocessor that adds powerful features like variables, nesting, mixins, and functions to standard CSS.

Overview

SCSS is the most widely-used syntax of Sass, fully compatible with CSS while extending it with programming features. It compiles to standard CSS and helps create maintainable, scalable stylesheets through modularisation and reusable patterns.

Deprecation note: Dart Sass is phasing out the global built-in functions (map-get, darken/lighten, type-of, unit, nth, etc.) in favour of the sass:* modules (map.get, color.adjust/color.scale, meta.type-of, math.unit, list.nth). Likewise, use math.div($a, $b) rather than $a / $b for division. The global forms still compile on Dart Sass 1.x (with warnings) but are slated for removal in Dart Sass 3.0 — prefer the module forms (@use 'sass:map', @use 'sass:color', @use 'sass:math') in new code.

SCSS FilesSass CompilerCSS OutputSource MapsBrowserSCSS FilesSass CompilerCSS OutputSource MapsBrowser

Nesting and Partials

Nesting

Nesting allows you to write CSS that mirrors HTML structure, improving readability and reducing repetition.

// Basic nesting
.navbar {
  background: #333;
  padding: 1rem;

  ul {
    list-style: none;
    margin: 0;
  }

  li {
    display: inline-block;
  }

  a {
    color: white;
    text-decoration: none;

    &:hover {
      color: #00bcd4;
    }
  }
}

Parent Selector (&)

The & references the parent selector, essential for pseudo-classes and BEM methodology.

.button {
  background: blue;

  // Pseudo-classes
  &:hover {
    background: darkblue;
  }

  &:active {
    transform: scale(0.98);
  }

  // BEM modifiers
  &--primary {
    background: #007bff;
  }

  &--secondary {
    background: #6c757d;
  }

  // BEM elements
  &__icon {
    margin-right: 0.5rem;
  }

  // Compound selectors
  &.is-disabled {
    opacity: 0.5;
    pointer-events: none;
  }
}

Partials and Imports

Partials are SCSS files prefixed with _ that are meant to be imported, not compiled directly.

main.scss_variables.scss_mixins.scss_base.scss_components.scss_buttons.scss_cards.scss_forms.scssmain.scss_variables.scss_mixins.scss_base.scss_components.scss_buttons.scss_cards.scss_forms.scss
// File: _variables.scss
$primary-colour: #007bff;
$font-stack: 'Helvetica', sans-serif;

// File: _mixins.scss
@mixin flex-centre {
  display: flex;
  justify-content: center;
  align-items: center;
}

// File: main.scss
@use 'variables';
@use 'mixins';

.container {
  color: variables.$primary-colour;
  @include mixins.flex-centre;
}

@use vs @import

The @use rule is the modern replacement for @import.

// @use - recommended (namespaced by default)
@use 'variables';
@use 'mixins' as m;
@use 'functions' as *;  // Load without namespace

.element {
  color: variables.$primary-colour;
  @include m.flex-centre;
}

// @forward - re-export from partials
// File: _index.scss
@forward 'variables';
@forward 'mixins';
@forward 'functions';

Variables and Mixins

Variables

Variables store reusable values with the $ prefix.

// Basic variables
$primary-colour: #007bff;
$secondary-colour: #6c757d;
$font-size-base: 16px;
$spacing-unit: 8px;

// Maps for organised values
$colours: (
  'primary': #007bff,
  'secondary': #6c757d,
  'success': #28a745,
  'danger': #dc3545,
  'warning': #ffc107
);

$breakpoints: (
  'sm': 576px,
  'md': 768px,
  'lg': 992px,
  'xl': 1200px
);

// Accessing map values
.alert {
  background: map-get($colours, 'warning');
}

// Default values (can be overridden)
$border-radius: 4px !default;

// Variable scope
$global-var: 'I am global';

.selector {
  $local-var: 'I am local';
  // $global-var is accessible here
}
// $local-var is NOT accessible here

Mixins

Mixins are reusable blocks of styles that can accept arguments.

// Basic mixin
@mixin reset-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

// Mixin with arguments
@mixin button-variant($bg-colour, $text-colour: white) {
  background-color: $bg-colour;
  color: $text-colour;
  border: 2px solid darken($bg-colour, 10%);

  &:hover {
    background-color: darken($bg-colour, 10%);
  }
}

// Mixin with variable arguments
@mixin box-shadow($shadows...) {
  box-shadow: $shadows;
}

// Usage
.nav-list {
  @include reset-list;
}

.btn-primary {
  @include button-variant(#007bff);
}

.btn-warning {
  @include button-variant(#ffc107, #333);
}

.card {
  @include box-shadow(
    0 2px 4px rgba(0, 0, 0, 0.1),
    0 4px 8px rgba(0, 0, 0, 0.1)
  );
}

Content Blocks

Mixins can accept content blocks with @content.

// Media query mixin
@mixin respond-to($breakpoint) {
  @if map-has-key($breakpoints, $breakpoint) {
    @media (min-width: map-get($breakpoints, $breakpoint)) {
      @content;
    }
  } @else {
    @warn "Unknown breakpoint: #{$breakpoint}";
  }
}

// Hover states for devices that support it
@mixin hover-supported {
  @media (hover: hover) {
    &:hover {
      @content;
    }
  }
}

// Usage
.container {
  width: 100%;

  @include respond-to('md') {
    width: 750px;
  }

  @include respond-to('lg') {
    width: 970px;
  }
}

.button {
  @include hover-supported {
    transform: translateY(-2px);
  }
}

Inheritance and Extend

Placeholder Selectors

Placeholders (%) define styles that won't output unless extended.

// Placeholder selector
%button-base {
  display: inline-block;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  font-weight: bold;
  text-align: center;
  cursor: pointer;
  transition: all 0.2s ease;
}

%visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

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

.btn-primary {
  @extend %button-base;
  background: #007bff;
  color: white;
}

.btn-secondary {
  @extend %button-base;
  background: #6c757d;
  color: white;
}

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

Extend vs Mixin

YesNoYesNoReusable Styles?Need Arguments?Use MixinSame StylesEverywhere?UseExtend/PlaceholderUse Mixin with@contentYesNoYesNoReusable Styles?Need Arguments?Use MixinSame StylesEverywhere?UseExtend/PlaceholderUse Mixin with@content
Feature @extend @mixin
Arguments No Yes
@content blocks No Yes
Output Grouped selectors Duplicated styles
Use case Identical styles Variations of styles
// When to use @extend - identical styles
%message-shared {
  border: 1px solid #ccc;
  padding: 10px;
  color: #333;
}

.message { @extend %message-shared; }
.success { @extend %message-shared; }
.error { @extend %message-shared; }

// Compiles to:
// .message, .success, .error {
//   border: 1px solid #ccc;
//   padding: 10px;
//   color: #333;
// }

// When to use @mixin - variations needed
@mixin message-variant($colour) {
  border: 1px solid $colour;
  padding: 10px;
  color: darken($colour, 20%);
}

.success { @include message-variant(#28a745); }
.error { @include message-variant(#dc3545); }

Functions and Operators

Built-in Functions

// Colour functions
$base-colour: #007bff;

.element {
  background: $base-colour;
  border-color: darken($base-colour, 15%);
  color: lighten($base-colour, 40%);
}

.overlay {
  background: rgba($base-colour, 0.5);
  border: 1px solid adjust-hue($base-colour, 180deg);
}

.desaturated {
  background: desaturate($base-colour, 50%);
}

// Colour inspection
$is-light: lightness($base-colour) > 50%;

// String functions
$font-path: '/fonts/';
$font-name: 'Roboto';

@font-face {
  font-family: quote($font-name);
  src: url($font-path + to-lower-case($font-name) + '.woff2');
}

// Number functions
.element {
  width: percentage(0.5);      // 50%
  height: round(10.6px);       // 11px
  padding: ceil(4.2px);        // 5px
  margin: floor(4.8px);        // 4px
  min-height: max(100px, 10vh);
}

// List functions
$sizes: 10px, 20px, 30px;

.element {
  margin-top: nth($sizes, 1);      // 10px
  margin-bottom: nth($sizes, -1);  // 30px (last item)
  padding: append($sizes, 40px);   // 10px, 20px, 30px, 40px
}

// Map functions
$theme: (
  'light': #fff,
  'dark': #333
);

.themed {
  @if map-has-key($theme, 'light') {
    background: map-get($theme, 'light');
  }
}

$merged: map-merge($theme, ('accent': #007bff));

Custom Functions

@use 'sass:math';

// Custom function
@function calculate-rem($size-px, $base: 16px) {
  @return math.div($size-px, $base) * 1rem;
}

@function colour-contrast($colour) {
  $luminance: lightness($colour);
  @return if($luminance > 50%, #000, #fff);
}

@function spacing($multiplier: 1) {
  @return $multiplier * 8px;
}

// Usage
.heading {
  font-size: calculate-rem(24px);     // 1.5rem
  margin-bottom: spacing(2);           // 16px
}

.button {
  background: $primary-colour;
  color: colour-contrast($primary-colour);
}

Operators

@use 'sass:math';

// Arithmetic operators
.container {
  width: calc(100% - 20px); // mixed units (% and px) must stay in calc()
  padding: 10px + 5px;
  margin: math.div(30px, 2);    // `/` for division is deprecated; use math.div
  font-size: 16px * 1.5;
}

// Modulo
@for $i from 1 through 6 {
  .item-#{$i} {
    @if $i % 2 == 0 {
      background: #f0f0f0;
    }
  }
}

// Comparison operators
$padding: 20px;

.element {
  @if $padding >= 15px {
    padding: $padding;
  } @else {
    padding: 15px;
  }
}

// String concatenation
$icon-path: '/images/icons/';
$icon-name: 'arrow';

.icon {
  background-image: url($icon-path + $icon-name + '.svg');
}

Control Directives

@if, @else if, @else

// Basic conditional
@mixin theme-colours($theme) {
  @if $theme == 'light' {
    background: white;
    color: #333;
  } @else if $theme == 'dark' {
    background: #333;
    color: white;
  } @else {
    background: #f5f5f5;
    color: #666;
  }
}

// Multiple conditions
@mixin responsive-font($size) {
  @if $size < 14px {
    font-size: 14px;
    @warn "Font size too small, using minimum 14px";
  } @else if $size > 72px {
    font-size: 72px;
    @warn "Font size too large, using maximum 72px";
  } @else {
    font-size: $size;
  }
}

// Null checks
$custom-colour: null;

.element {
  @if $custom-colour {
    color: $custom-colour;
  } @else {
    color: inherit;
  }
}

@for Loop

@use 'sass:math';

// through includes end value
@for $i from 1 through 5 {
  .col-#{$i} {
    width: percentage(math.div($i, 5));
  }
}

// to excludes end value
@for $i from 0 to 5 {
  .mt-#{$i} {
    margin-top: $i * 8px;
  }
}

// Generate z-index scale
@for $i from 1 through 10 {
  .z-#{$i * 10} {
    z-index: $i * 10;
  }
}

@each Loop

// Iterate over list
$colours: red, green, blue;

@each $colour in $colours {
  .text-#{$colour} {
    color: $colour;
  }
}

// Iterate over map
$social-colours: (
  'facebook': #3b5998,
  'twitter': #1da1f2,
  'linkedin': #0077b5,
  'instagram': #e4405f
);

@each $network, $colour in $social-colours {
  .btn-#{$network} {
    background-color: $colour;

    &:hover {
      background-color: darken($colour, 10%);
    }
  }
}

// Destructuring lists
$icons: (
  ('home', '\e900'),
  ('user', '\e901'),
  ('search', '\e902')
);

@each $name, $code in $icons {
  .icon-#{$name}::before {
    content: $code;
  }
}

@while Loop

// Use sparingly - prefer @for when possible
$i: 6;

@while $i > 0 {
  .heading-#{$i} {
    font-size: 12px + ($i * 4px);
  }
  $i: $i - 1;
}

Output Styles

Compilation Options

SCSS SourceOutput StyleexpandedcompressedDevelopmentReadable, debuggableProductionMinified, optimisedSCSS SourceOutput StyleexpandedcompressedDevelopmentReadable, debuggableProductionMinified, optimised

Style Comparison

// Source SCSS
.button {
  background: blue;

  &:hover {
    background: darkblue;
  }
}

Expanded (default for development)

.button {
  background: blue;
}
.button:hover {
  background: darkblue;
}

Compressed (for production)

.button{background:blue}.button:hover{background:darkblue}

Compilation Commands

# Dart Sass CLI
sass --style=expanded input.scss output.css
sass --style=compressed input.scss output.min.css

# Watch with style
sass --watch --style=compressed src/scss:dist/css

# Node-sass (deprecated)
node-sass --output-style compressed input.scss output.css

# Webpack (sass-loader)
# In webpack.config.js
{
  loader: 'sass-loader',
  options: {
    sassOptions: {
      outputStyle: 'compressed'
    }
  }
}

Source Maps

Overview

Source maps link compiled CSS back to original SCSS for debugging in browser dev tools.

Browser DevToolsSource MapOriginal SCSSLine NumbersFile NamesBrowser DevToolsSource MapOriginal SCSSLine NumbersFile Names

Generating Source Maps

# Dart Sass - source maps enabled by default
sass input.scss output.css

# Disable source maps
sass --no-source-map input.scss output.css

# Embed the source map inline in the CSS (data URI)
sass --embed-source-map input.scss output.css

# Use absolute URLs from the source map to the source files
# (default is 'relative'; only 'relative' and 'absolute' are valid)
sass --source-map-urls=absolute input.scss output.css

Configuration

// Webpack sass-loader
{
  loader: 'sass-loader',
  options: {
    sourceMap: true,
    sassOptions: {
      outputStyle: 'expanded'
    }
  }
}

// Vite
export default {
  css: {
    devSourcemap: true,
    preprocessorOptions: {
      scss: {
        sourceMap: true
      }
    }
  }
}

// Gulp
const sass = require('gulp-sass')(require('sass'));

gulp.task('sass', () => {
  return gulp.src('src/**/*.scss')
    .pipe(sass({ sourceMap: true }).on('error', sass.logError))
    .pipe(gulp.dest('dist'));
});

Source Map Structure

{
  "version": 3,
  "sourceRoot": "",
  "sources": ["input.scss", "_variables.scss", "_mixins.scss"],
  "names": [],
  "mappings": "AAAA;EACE,..."
}

Common Patterns for Maintainable Stylesheets

File Organisation (7-1 Pattern)

scss/
├── abstracts/
│   ├── _variables.scss
│   ├── _functions.scss
│   ├── _mixins.scss
│   └── _index.scss
├── base/
│   ├── _reset.scss
│   ├── _typography.scss
│   └── _index.scss
├── components/
│   ├── _buttons.scss
│   ├── _cards.scss
│   ├── _forms.scss
│   └── _index.scss
├── layout/
│   ├── _header.scss
│   ├── _footer.scss
│   ├── _grid.scss
│   └── _index.scss
├── pages/
│   ├── _home.scss
│   ├── _contact.scss
│   └── _index.scss
├── themes/
│   ├── _dark.scss
│   └── _index.scss
├── vendors/
│   └── _index.scss
└── main.scss

Responsive Design Pattern

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

@mixin media($breakpoint) {
  @if map-has-key($breakpoints, $breakpoint) {
    @media (min-width: map-get($breakpoints, $breakpoint)) {
      @content;
    }
  }
}

@mixin media-down($breakpoint) {
  @if map-has-key($breakpoints, $breakpoint) {
    @media (max-width: map-get($breakpoints, $breakpoint) - 1px) {
      @content;
    }
  }
}

// Usage
.sidebar {
  width: 100%;

  @include media('md') {
    width: 250px;
  }

  @include media('lg') {
    width: 300px;
  }
}

BEM Methodology

// Block
.card {
  background: white;
  border-radius: 8px;

  // Element
  &__header {
    padding: 1rem;
    border-bottom: 1px solid #eee;
  }

  &__body {
    padding: 1rem;
  }

  &__footer {
    padding: 1rem;
    background: #f5f5f5;
  }

  // Modifier
  &--featured {
    border: 2px solid gold;
  }

  &--compact {
    .card__body {
      padding: 0.5rem;
    }
  }
}

Theme System

// _themes.scss
$themes: (
  'light': (
    'bg-primary': #ffffff,
    'bg-secondary': #f5f5f5,
    'text-primary': #333333,
    'text-secondary': #666666,
    'border': #dddddd
  ),
  'dark': (
    'bg-primary': #1a1a1a,
    'bg-secondary': #2d2d2d,
    'text-primary': #ffffff,
    'text-secondary': #aaaaaa,
    'border': #444444
  )
);

@mixin themed {
  @each $theme-name, $theme-map in $themes {
    [data-theme='#{$theme-name}'] & {
      $theme: $theme-map !global;
      @content;
    }
  }
}

@function t($key) {
  @return map-get($theme, $key);
}

// Usage
.card {
  @include themed {
    background: t('bg-primary');
    color: t('text-primary');
    border: 1px solid t('border');
  }
}

Utility Classes Generator

// Generate spacing utilities
$spacing-values: (0, 1, 2, 3, 4, 5);
$spacing-unit: 8px;

@each $value in $spacing-values {
  .m-#{$value} { margin: $value * $spacing-unit; }
  .mt-#{$value} { margin-top: $value * $spacing-unit; }
  .mb-#{$value} { margin-bottom: $value * $spacing-unit; }
  .ml-#{$value} { margin-left: $value * $spacing-unit; }
  .mr-#{$value} { margin-right: $value * $spacing-unit; }
  .mx-#{$value} {
    margin-left: $value * $spacing-unit;
    margin-right: $value * $spacing-unit;
  }
  .my-#{$value} {
    margin-top: $value * $spacing-unit;
    margin-bottom: $value * $spacing-unit;
  }

  .p-#{$value} { padding: $value * $spacing-unit; }
  .pt-#{$value} { padding-top: $value * $spacing-unit; }
  .pb-#{$value} { padding-bottom: $value * $spacing-unit; }
  .pl-#{$value} { padding-left: $value * $spacing-unit; }
  .pr-#{$value} { padding-right: $value * $spacing-unit; }
}

Quick Reference

Variables and Functions

Feature Syntax Example
Variable $name: value; $colour: #007bff;
Map $map: (key: value); $colours: ('primary': blue);
Map get map-get($map, key) map-get($colours, 'primary')
Function @function name() {} @function rem($px) { @return $px/16*1rem; }
Darken darken($colour, %) darken(#007bff, 10%)
Lighten lighten($colour, %) lighten(#007bff, 20%)
RGBA rgba($colour, $alpha) rgba(#007bff, 0.5)

Mixins and Extend

Feature Syntax Usage
Mixin @mixin name {} @include name;
Mixin with args @mixin name($arg) {} @include name(value);
Content block @content @include name { styles }
Placeholder %name {} Define reusable block
Extend @extend %name; Inherit placeholder styles

Control Directives

Directive Syntax Example
If @if condition {} @if $x > 10 { }
Else @else {} @else { }
For (through) @for $i from 1 through 5 Includes 5
For (to) @for $i from 1 to 5 Excludes 5
Each (list) @each $item in $list Iterate list
Each (map) @each $k, $v in $map Iterate map

Module System

Feature Syntax Note
Use @use 'file'; Namespaced import
Use as @use 'file' as f; Custom namespace
Use global @use 'file' as *; No namespace
Forward @forward 'file'; Re-export module

Common Issues and Solutions

Issue Cause Solution
Division not working Sass deprecated / for division Use math.div($a, $b) (@use 'sass:math')
Variables undefined Scope or import issues Check @use statements and namespaces
@extend not working Extending class not in scope Use placeholders % or check imports
Source maps not showing Not generated or wrong path Check compiler flags and sourceRoot
Nested selectors too specific Over-nesting Limit nesting to 3-4 levels max
Large CSS output Overuse of @extend on classes Use placeholders or mixins instead
@import deprecation warnings Old import syntax Migrate to @use and @forward
Map key not found Typo or missing key Use map-has-key() before map-get()
Interpolation in calc Variables need interpolation Use calc(100% - #{$var})
Colour function errors Invalid colour format Ensure proper hex, rgb, or variable

Migration from @import to @use

// Old way (deprecated)
@import 'variables';
@import 'mixins';

// New way
@use 'variables';
@use 'mixins';

// Access with namespace
.element {
  color: variables.$primary-colour;
  @include mixins.flex-centre;
}

// Or configure namespace
@use 'variables' as v;
@use 'mixins' as *;  // No namespace

.element {
  color: v.$primary-colour;
  @include flex-centre;
}

Debugging Tips

// Print debug messages
@debug "Current colour: #{$colour}";

// Print warnings
@warn "This value might cause issues";

// Print errors and stop compilation
@error "This value is not supported";

// Inspect variable types
@debug type-of($var);      // e.g., "color", "string", "number"
@debug unit($value);       // e.g., "px", "rem", "%"
@debug comparable($a, $b); // Can these be compared?