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 thesass:*modules (map.get,color.adjust/color.scale,meta.type-of,math.unit,list.nth). Likewise, usemath.div($a, $b)rather than$a / $bfor 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.
flowchart LR
A[SCSS Files] --> B[Sass Compiler]
B --> C[CSS Output]
B --> D[Source Maps]
C --> E[Browser]
D --> E
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.
flowchart TD
A[main.scss] --> B[_variables.scss]
A --> C[_mixins.scss]
A --> D[_base.scss]
A --> E[_components.scss]
E --> F[_buttons.scss]
E --> G[_cards.scss]
E --> H[_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
flowchart TD
A[Reusable Styles?] --> B{Need Arguments?}
B -->|Yes| C[Use Mixin]
B -->|No| D{Same Styles Everywhere?}
D -->|Yes| E[Use Extend/Placeholder]
D -->|No| F[Use 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
flowchart TD
A[SCSS Source] --> B{Output Style}
B --> C[expanded]
B --> D[compressed]
C --> E["Development<br/>Readable, debuggable"]
D --> F["Production<br/>Minified, 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.
flowchart LR
A[Browser DevTools] --> B[Source Map]
B --> C[Original SCSS]
B --> D[Line Numbers]
B --> E[File 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?