99

Concurrent visitors over the last 30 minutes. (~)

Peak concurrent visitors -
Yesterday: 0 / Today: 0

The Complete Guide to SCSS/Sass

The Complete Guide to SCSS/Sass

# Overview

This post is based on Dart Sass 1.104.
Explanations use SCSS, and Sass is explained separately where there are differences.

CSS is relatively easy to learn and fun.
For web development beginners, nothing is quite as approachable.

CSS is certainly easy and fun, but as your work gets more advanced, the inconveniences grow too.
Overusing unnecessary selectors, limited math features, a lack of statements: as the project grows, so does your frustration.
But on the web, only standard CSS can run, so there's no other choice.

So does that mean we have to keep using plain CSS forever?

# CSS preprocessors

If you work with HTML and CSS, you've probably heard of Sass, Less, and similar tools at least once.
These tools process CSS ahead of time.
They're commonly called CSS preprocessors.

They're used before the CSS runs,
so even though the web only runs CSS, we can make up for its inconveniences with these extensions.

Sass is an extension of CSS that adds power and elegance to the basic language.

Standard CSS has also come a long way.
Custom properties (--color: red) let you use values like variables, and selector nesting works in all modern browsers.
Even so, we keep using preprocessors because standard CSS has no features that generate CSS at compile time, like a module system, mixins, and loops, and new CSS features take time to work in every browser.

# How it works

As mentioned above, only CSS runs on the web.
Preprocessors like Sass, Less, and Stylus (called 'preprocessors' from here on) can't run directly.
So how can we use them?

Since CSS is inconvenient, set it aside for now and write (code) in a preprocessor first.
Preprocessor syntax is very similar to CSS, but you can write more easily with many more features than standard CSS, such as selector nesting, conditionals, loops, and math across different units.
However, it doesn't run directly on the web, so you compile what you wrote into standard CSS that can run on the web.
You write in a preprocessor, compile it to CSS, and run that.

# Sass and SCSS

SCSS, introduced in version 3 of Sass (Syntactically Awesome Style Sheets), is a superset of CSS that supports every Sass feature, built with a new syntax that is fully compatible with CSS syntax.
In other words, SCSS supports Sass features with almost the same syntax as CSS.

The easiest way to tell them apart is whether they use {} (curly braces) and ; (semicolons).
Compare the examples below.

Sass:

SASS
1
2
3
4
5
6
7
8
.list width: 100px float: left li color: red background: url("./image.jpg") &:last-child margin-right: -10px

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
.list { width: 100px; float: left; li { color: red; background: url("./image.jpg"); &:last-child { margin-right: -10px; } } }

Sass marks the scope of a selector with 'indentation', while SCSS marks it with {}.
Which style do you like better, Sass or SCSS?

That's almost the only difference, but there is one more.
Below is a mixins example ('mixins' are a way to create reusable features).
Sass uses a shorthand syntax.

Sass:

SASS
1
2
3
4
5
6
7
=flex-center display: flex justify-content: center align-items: center .box +flex-center

SCSS:

SCSS
1
2
3
4
5
6
7
@mixin flex-center { display: flex; justify-content: center; align-items: center; } .box { @include flex-center; }

Sass used the = and + symbols for mixins,
while SCSS used @mixin and @include.

Apart from a few simple things, there's little difference, but with complex code, each may have pros and cons depending on the environment.
Sass is more concise and easier to write, and since you don't need {} or ;, the code looks much cleaner.
SCSS lets you write inline code (on a single line), and since its syntax is similar to CSS, it's much easier to integrate with existing code.

Because of these pros and cons, you may have to use whichever style your company or team prefers, or you can choose based on personal taste.
Just pick the style that fits the situation and that you like.

In most cases, we recommend SCSS.

# How to compile

Sass (SCSS) can't run directly on the web.
In the end, it has to run as standard CSS, so we write in the preprocessor and then compile it to CSS.

Today, the only officially maintained implementation is Dart Sass.
Ruby Sass and LibSass (node-sass), which were used in the past, are both no longer supported, so use the sass or sass-embedded package.
sass is Dart Sass compiled to JavaScript, and sass-embedded runs a natively compiled Dart Sass as a separate process.
The syntax is the same, but sass-embedded compiles faster.

# Sass Playground

For a small piece of Sass code, installing a compiler may feel like too much.
In that case, you can use the Sass Playground provided by the official docs.

Open the page and start writing Sass or SCSS, and it's converted to CSS in real time.
It's handy for checking syntax or comparing compiled output, so it will help you learn.

# Sass CLI

Once you install the sass package, you can compile directly with a command.

BASH
1
npm i -D sass
Install

You can compile a single file or an entire directory.

BASH
1
2
3
4
5
## Compile `main.scss` into `main.css` npx sass main.scss main.css ## Compile the files in the `scss` directory into the `css` directory npx sass scss:css

Add the --watch option to recompile every time a file changes.

BASH
1
npx sass --watch scss:css

# Vite

Vite supports Sass (SCSS) without any extra configuration.
Just install a compiler package and you can use it right away.

BASH
1
npm i -D sass-embedded

If sass-embedded is installed, Vite uses it first; otherwise, it uses sass.

# Webpack

Install sass-loader together with a compiler package.

BASH
1
npm i -D style-loader css-loader sass-loader sass-embedded
/webpack.config.js
JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = { module: { rules: [ { test: /\.s?[ac]ss$/i, use: [ 'style-loader', 'css-loader', 'sass-loader' ] } ] } }

# Parcel

With Parcel, you only need to link your SCSS/Sass file in the HTML with <link>.
No other setup is needed.

BASH
1
npm i -D parcel sass
/index.html
HTML
1
<link rel="stylesheet" href="./src/main.scss">
BASH
1
npx parcel index.html

# Comments

A CSS comment is /* ... */.
Sass (SCSS) uses two styles of comments, like JavaScript.

SCSS
1
2
// A comment that is not compiled /* A comment that is compiled */

In Sass, when you write a multi-line comment that gets compiled, you must put * at the start of each line, and importantly, the * characters must line up.

Sass:

SASS
1
2
3
4
5
6
7
8
/* This comment * spans multiple lines * and is compiled */ // Error /* This comment * spans multiple lines * and is compiled */

In SCSS, it's fine even without * on each line.
This makes it easy to stay compatible with existing CSS.

SCSS:

SCSS
1
2
3
4
5
/* This comment spans multiple lines and is compiled */

# Data types

Data Description Example
Numbers Numbers 1, .82, 20px, 2em
Strings Text bold, relative, "/images/a.png", "dotum"
Colors Color values red, blue, #FFFF00, rgba(255, 0, 0, .5)
Booleans Logical values true, false
Nulls Nothing at all null
Lists A list of values separated by spaces or , (apple, orange, banana), apple orange
Maps Like Lists, but values take the form Key: Value (apple: a, orange: o, banana: b)

# Things to note

Here are a few things to note about the data types used in Sass.

  • Numbers: May or may not have a unit.
  • Strings: May or may not have quotes.
  • Nulls: If null is used as a property value, that property isn't compiled.
  • Lists: May or may not be wrapped in ().
  • Maps: Must be wrapped in ().

Besides rgb and hsl, Colors also support the color spaces defined in CSS Color 4, such as oklch, lab, and display-p3.
Each color space has different channel names and ranges, so use functions from the sass:color module when calculating color values.

# Nesting

Sass supports nesting.
It lets you avoid repeating parent selectors and write complex structures more easily.

Nesting has also been added to standard CSS and works in all modern browsers.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
.section { width: 100%; .list { padding: 20px; li { float: left; } } }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
.section { width: 100%; } .section .list { padding: 20px; } .section .list li { float: left; }

# Ampersand (&)

Inside nesting, the & keyword refers to the parent selector and is replaced by it.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.btn { position: absolute; &.active { color: red; } } .list { li { &:last-child { margin-right: 0; } } }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
.btn { position: absolute; } .btn.active { color: red; } .list li:last-child { margin-right: 0; }

Since & is replaced by the parent selector it refers to, you can also use it like this.

SCSS:

SCSS
1
2
3
4
5
.fs { &-small { font-size: 12px; } &-medium { font-size: 14px; } &-large { font-size: 16px; } }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
.fs-small { font-size: 12px; } .fs-medium { font-size: 14px; } .fs-large { font-size: 16px; }

# @at-root

Use the @at-root keyword when you want to break out of nesting.
It's useful when you create something inside nesting but need to use it outside.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
.list { $w: 100px; $h: 50px; li { width: $w; height: $h; } @at-root .box { width: $w; height: $h; } }

Compiled to:

CSS
1
2
3
4
5
6
7
8
.list li { width: 100px; height: 50px; } .box { width: 100px; height: 50px; }

As the example below shows, variables inside .list can't be used outside their scope, so you need the @at-root keyword as in the example above. (Variables are explained below.)

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.list { $w: 100px; $h: 50px; li { width: $w; height: $h; } } // Error .box { width: $w; height: $h; }

# Nested properties

Properties that share the same namespace, such as font- and margin-, can be written like this.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.box { font: { weight: bold; size: 10px; family: sans-serif; }; margin: { top: 10px; left: 20px; }; padding: { bottom: 40px; right: 30px; }; }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
.box { font-weight: bold; font-size: 10px; font-family: sans-serif; margin-top: 10px; margin-left: 20px; padding-bottom: 40px; padding-right: 30px; }

# Variables

You can store values you use repeatedly in variables.
Always put $ in front of a variable name.

SCSS
1
$variable-name: value;

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
$color-primary: #e96900; $url-images: "/assets/images/"; $w: 200px; .box { width: $w; margin-left: $w; background: $color-primary url($url-images + "bg.jpg"); }

Compiled to:

CSS
1
2
3
4
5
.box { width: 200px; margin-left: 200px; background: #e96900 url("/assets/images/bg.jpg"); }

# Variable scope

Variables have a scope in which they can be used.
They're only valid inside the block ({}) where they're declared.

The variable $color is set inside the .box1 block, so it can't be used in .box2, which is outside that block.

SCSS
1
2
3
4
5
6
7
8
.box1 { $color: #111; background: $color; } // Error .box2 { background: $color; }

# Variable reassignment

You can reassign a variable to another variable like this.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
$red: #FF0000; $blue: #0000FF; $color-primary: $blue; $color-danger: $red; .box { color: $color-primary; background: $color-danger; }

Compiled to:

CSS
1
2
3
4
.box { color: #0000FF; background: #FF0000; }

# !global

The !global flag lets you change the value of a global variable from inside a block.
Without !global, a variable assigned inside a block becomes a local variable, separate from the global variable with the same name.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
$color: #000; .box1 { $color: #111 !global; background: $color; } .box2 { background: $color; } .box3 { $color: #222; background: $color; }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
.box1 { background: #111; } .box2 { background: #111; } .box3 { background: #222; }

Because .box1 changed the value of the global variable $color with !global, .box2, which comes after it, also uses the changed value.
On the other hand, $color in .box3 is a local variable, so the global variable's value stays #111.

Using !global to create a new variable that wasn't declared at the top level is deprecated, and it won't work starting with Dart Sass 2.0.0.
For a global variable, declare it at the top level first, like $color: null, and then assign a value with !global.

# !default

The !default flag sets an initial value for a variable that hasn't been assigned yet.
In other words, if the variable already has a value, the existing value is kept, and the value set with !default is used only when there isn't one.

SCSS:

SCSS
1
2
3
4
5
6
$color-primary: red; .box { $color-primary: blue !default; background: $color-primary; }

Compiled to:

CSS
1
2
3
.box { background: red; }

!default is mainly used in libraries and in modules shared across many places.
If you add !default to a variable in a module, the code that loads the module can change it to whatever value it wants.
On the other hand, a variable without !default can't be changed from outside the module.

/scss/_config.scss
SCSS
1
2
$color-active: red !default; $size-base: 16px !default;

Here is part of the Bootstrap code.
Every variable has !default, so code that uses Bootstrap can change only the values it wants.

/scss/_variables.scss
SCSS
1
2
3
4
5
6
$white: #fff !default; $gray-100: #f8f9fa !default; $blue: #0d6efd !default; $primary: $blue !default; $font-size-base: 1rem !default; $border-radius: 0.375rem !default;

How to change a variable's value while loading a module is covered in Configuring modules.

# #{}

With #{}, you can insert a variable's value anywhere in your code.
This is called interpolation.

SCSS
1
2
3
4
@use "sass:string"; $family: string.unquote("Noto+Sans+KR"); @import url("https://fonts.googleapis.com/css2?family=#{$family}");
CSS
1
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR");

The string.unquote() function from the sass:string module removes quotes from a string.
Built-in functions are covered in detail in Built-in modules.

# Modules

As a project grows, you split files by feature and part, such as header and side-menu, to keep them easy to maintain.
You load these split files with @use and combine them into a single CSS file.

In the past, files were loaded with @import, but Sass's @import has been deprecated since Dart Sass 1.80.0 and is scheduled to be removed in Dart Sass 3.0.0.
Use @use and @forward in new code.

# Partials

As you split files, you end up with more and more of them, and if every file were compiled into its own ~.css file, it could cause problems for management and performance.
That's why Sass supports partials.
If you put _ in front of a file name (like _header.scss), that file isn't compiled into its own ~.css file; instead, other files load and use it.

Let's look at an example.
Say there are three Sass files in scss/, like this.

BASH
1
2
3
4
5
6
7
Sass-App # ... ├─scss │ ├─_header.scss │ ├─_side-menu.scss │ └─main.scss # ...

main.scss loads the other two files.
When loading them, leave out the _ and the extension from the file name.

/scss/main.scss
SCSS
1
2
@use "header"; @use "side-menu";

Then compile these files into the css/ directory.

BASH
1
npx sass scss:css

After compiling, you'll see that the files starting with _ aren't compiled into separate files but are combined into a single main.css, as shown below.

BASH
1
2
3
4
5
6
7
8
9
Sass-App # ... ├─css │ └─main.css # main + header + side-menu ├─scss │ ├─_header.scss │ ├─_side-menu.scss │ └─main.scss # ...

Without _, header.css and side-menu.css are created as well.
Build tools like Webpack and Vite compile only a single entry file, so they don't have this problem, but it's still recommended to use _ whenever you can.

# @use

Variables, mixins, and functions from a module loaded with @use are accessed with the file name as a namespace.
Unlike @import, names aren't all mixed together in the global scope, so you can tell which file a name came from just by reading the code.

/scss/_config.scss
SCSS
1
2
3
4
5
6
7
8
$primary: royalblue; $radius: 4px; @mixin flex-center { display: flex; justify-content: center; align-items: center; }
/scss/main.scss
SCSS
1
2
3
4
5
6
7
@use "config"; .btn { background: config.$primary; border-radius: config.$radius; @include config.flex-center; }

You can rename the namespace with as.

SCSS
1
2
3
4
5
@use "config" as c; .btn { background: c.$primary; }

With as *, you can use the module without a namespace.
It's convenient, but names can collide, so it's best to avoid it when you can.

SCSS
1
2
3
4
5
@use "config" as *; .btn { background: $primary; }

Even if you load the same file with @use from many places, it's compiled only once, so no duplicate CSS is generated.

@use must be written at the very top of the file.
It must come before every other rule except @charset and comments, and it can't be used inside a selector block.
However, variables to pass when configuring a module can be declared before @use.

If you put _ or - in front of a name, that variable, mixin, or function can be used only inside its own file.
Use this to mark names that you don't want to expose outside the module.

/scss/_config.scss
SCSS
1
2
$-inner-gap: 4px; // Can't be accessed as `config.$-inner-gap` $gap: $-inner-gap * 2;

# Configuring modules

Variables with the !default flag can be loaded with different values using with.
Variables without !default can't be configured with with.

/scss/_config.scss
SCSS
1
2
$primary: royalblue !default; $radius: 4px !default;
/scss/main.scss
SCSS
1
2
3
4
5
6
7
8
9
@use "config" with ( $primary: #e96900, $radius: 8px ); .btn { background: config.$primary; // #e96900 border-radius: config.$radius; // 8px }

A module can be configured only once.
If another file has already loaded a module with @use and you configure it again with with, you get an error.

# @forward

@forward re-exports the variables, mixins, and functions of another file as they are.
Use it to bundle several partial files into a single entry point.

/scss/_index.scss
SCSS
1
2
3
@forward "config"; @forward "button"; @forward "header";

If you @use a directory name, it loads the _index.scss inside it.
No matter how many files are in scss/, the code that uses them can treat them as a single module.

/main.scss
SCSS
1
2
3
4
5
6
@use "scss" as ui; .btn { background: ui.$primary; @include ui.flex-center; }

You can choose what to export with show and hide, and you can also add a prefix with as.

SCSS
1
2
3
@forward "config" show $primary, flex-center; @forward "button" hide $btn-inner-gap; @forward "config" as config-*;

When you add a prefix with as config-*, variables are used like ui.$config-primary, and mixins and functions like ui.config-flex-center.

# @import

@import merges all the names from the loaded file into the global scope.
Names collide easily, it's hard to tell which file a name came from, and if you load the same file several times, the CSS is duplicated just as many times.
Because of these problems, it has been deprecated.

SCSS
1
2
// Deprecated syntax @import "header", "side-menu";

However, in the cases below, it's compiled as a CSS @import rule rather than Sass's @import, and you can keep using it.

  • When the file extension is .css
  • When the file name starts with http:// or https://
  • When it's written with url()
  • When it has a media query
SCSS
1
2
3
4
@import "hello.css"; @import "https://hello.com/hello"; @import url(hello); @import "hello" screen;

To bring the contents of a CSS file into your Sass code, write @use with the extension.

SCSS
1
@use "hello.css";

# Operations

Sass supports basic operations.
They're useful when you need to calculate sizes for a layout or divide a fixed value.
Here are the kinds of operators available in Sass.

Arithmetic operators:

Operator Description Note
+ Addition
- Subtraction
* Multiplication At least one value must be a number
% Remainder

For division, instead of an arithmetic operator, use the math.div() function from the sass:math module or calc().
We'll look at this in detail in the numbers section below.

Comparison operators:

Operator Description
== Equal to
!= Not equal to
< Comparison / less than
> Comparison / greater than
<= Comparison and equality / less than or equal to
>= Comparison and equality / greater than or equal to

Logical (Boolean) operators:

Operator Description
and And
or Or
not Not

# Numbers

Units that are the same or that can be converted to each other (px and in, em with em, and so on) can be used in operations directly.
But units that can't be converted at compile time, like % and px, can't be combined, so use CSS calc() in those cases.

SCSS
1
2
width: 50% - 20px; // Error: 50% and 20px have incompatible units. width: calc(50% - 20px); // Calculated in the browser

CSS allows / as a way to separate numbers in property values.
For example, in font: 16px / 22px serif;, / separates the property values font-size: 16px and line-height: 22px.
Because the meanings overlap like this, / was dropped as a division operator, and division is handled with the math.div() function or calc().
Existing code that divides with /, like $width / 2, still works, but a warning is printed every time you compile, and it's scheduled to be removed in Dart Sass 2.0.0.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
@use "sass:math"; div { width: 20px + 20px; // Addition height: 40px - 10px; // Subtraction font-size: 10px * 2; // Multiplication margin: math.div(30px, 2); // Division padding: calc(30px / 2); // Division }

Compiled to:

CSS
1
2
3
4
5
6
7
div { width: 40px; height: 30px; font-size: 20px; margin: 15px; padding: 15px; }

math.div() calculates the value at compile time.
calc() calculates at compile time when it can; otherwise, it outputs the CSS calc() as is.
So for things whose values aren't known at compile time, like CSS custom properties, you need to calculate with calc().

SCSS
1
2
3
div { width: calc(var(--size) / 2); }
CSS
1
2
3
div { width: calc(var(--size) / 2); }

# Strings

String operations use +.
The result of a string operation follows the first operand.
If the first operand has quotes, the result is wrapped in quotes.
On the other hand, if the first operand has no quotes, the result has no quotes either.

SCSS:

SCSS
1
2
3
4
div::after { content: "Hello " + World; flex-flow: row + "-reverse" + " " + wrap }

Compiled to:

CSS
1
2
3
4
div::after { content: "Hello World"; flex-flow: row-reverse wrap; }

# Colors

Color values can't be calculated with arithmetic operators.
Calculating channel by channel makes it hard to get the color you want and hard to predict the result, so color operations were removed.
Use functions from the sass:color module instead.

SCSS
1
2
3
4
div { // Error: Undefined operation "#123456 + #345678". color: #123456 + #345678; }

color.adjust() adds to or subtracts from channel values, color.scale() adjusts a channel by a percentage of its remaining range, and color.mix() mixes two colors.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
@use "sass:color"; $color: rgba(10, 20, 30, 0.5); div { color: color.adjust($color, $alpha: 0.3); // 30% more opaque / 0.5 + 0.3 background-color: color.adjust($color, $alpha: -0.2); // 20% more transparent / 0.5 - 0.2 border-color: color.scale(#336699, $lightness: 50%); // Lighter by 50% of the remaining lightness range outline-color: color.mix(red, blue, 30%); // Mix the two colors at 3:7 }

Compiled to:

CSS
1
2
3
4
5
6
div { color: rgba(10, 20, 30, 0.8); background-color: rgba(10, 20, 30, 0.3); border-color: rgb(55%, 70%, 85%); outline-color: rgb(30%, 0%, 70%); }

Older color functions like lighten(), darken(), opacify(), and transparentize() have been deprecated since Dart Sass 1.79.0 and are scheduled to be removed in Dart Sass 2.0.0.
Rewrite lighten($color, 10%) as color.adjust($color, $lightness: 10%), and transparentize($color, 0.2) as color.adjust($color, $alpha: -0.2).

# Logic

The logical (Boolean) operations used in Sass @if conditionals are 'and', 'or', and 'not'.
If you're familiar with JavaScript syntax, think of them as &&, ||, and !.

Let's look at a simple example here and cover the details in the conditionals section.

SCSS:

SCSS
1
2
3
4
5
6
$width: 90px; div { @if not ($width > 100px) { height: 300px; } }

Compiled to:

CSS
1
2
3
div { height: 300px; }

# Mixins

Sass mixins let you define groups of CSS declarations that you can reuse throughout your stylesheet.
With just a few mixins, you can create a wide variety of styles.

To start, you only need to remember two things about mixins.
They are declaring (@mixin) and including (@include).
You create one (declare) and then use it (include)!

# @mixin

A basic mixin declaration is very simple.
Define the styles with the @mixin directive.

SCSS:

SCSS
1
2
3
@mixin mixin-name { styles; }

Sass:

SASS
1
2
=mixin-name styles

SCSS:

SCSS
1
2
3
4
5
6
@mixin large-text { font-size: 22px; font-weight: bold; font-family: sans-serif; color: orange; }

A mixin can contain selectors and can also reference the parent element (with & and the like).

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@mixin large-text { font: { size: 22px; weight: bold; family: sans-serif; } color: orange; &::after { content: "!!"; } span.icon { background: url("/images/icon.png"); } }

# @include

To use (include) a declared mixin, you need @include.
Let's use the mixin we declared above.

SCSS:

SCSS
1
@include mixin-name;

Sass:

SASS
1
+mixin-name

SCSS:

SCSS
1
2
3
4
5
6
h1 { @include large-text; } div { @include large-text; }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
h1 { font-size: 22px; font-weight: bold; font-family: sans-serif; color: orange; } h1::after { content: "!!"; } h1 span.icon { background: url("/images/icon.png"); } div { font-size: 22px; font-weight: bold; font-family: sans-serif; color: orange; } div::after { content: "!!"; } div span.icon { background: url("/images/icon.png"); }

# Arguments

Like functions, mixins can take arguments.
A single mixin can produce many different results.

SCSS:

SCSS
1
2
3
4
@mixin mixin-name($parameter) { styles; } @include mixin-name(argument);

Sass:

SASS
1
2
3
4
=mixin-name($parameter) styles +mixin-name(argument)

A parameter is a kind of variable used to refer to one of the pieces of data provided.
The pieces of data provided are called arguments.

SCSS:

SCSS
1
2
3
4
5
6
@mixin dash-line($width, $color) { border: $width dashed $color; } .box1 { @include dash-line(1px, red); } .box2 { @include dash-line(4px, blue); }

Compiled to:

CSS
1
2
3
4
5
6
.box1 { border: 1px dashed red; } .box2 { border: 4px dashed blue; }

# Default argument values

Arguments can have default values.
If no argument is passed when you include the mixin with @include, the default value is used.

SCSS
1
2
3
@mixin mixin-name($parameter: default-value) { styles; }

SCSS:

SCSS
1
2
3
4
5
6
@mixin dash-line($width: 1px, $color: black) { border: $width dashed $color; } .box1 { @include dash-line; } .box2 { @include dash-line(4px); }

Compiled to:

CSS
1
2
3
4
5
6
.box1 { border: 1px dashed black; } .box2 { border: 4px dashed black; }

# Keyword arguments

SCSS
1
2
3
4
5
@mixin mixin-name($parameterA: default-value, $parameterB: default-value) { styles; } @include mixin-name($parameterB: argument);

When you pass arguments to a mixin, you can write the keyword (variable) explicitly; these are called keyword arguments.
You don't need to follow the order of the arguments, which makes them convenient to write.
However, it's a good idea to set default values so that the arguments you leave out still get a value.

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@mixin position( $p: absolute, $t: null, $b: null, $l: null, $r: null ) { position: $p; top: $t; bottom: $b; left: $l; right: $r; } .absolute { // Pass only the values to set, as keyword arguments @include position($b: 10px, $r: 20px); } .fixed { // Break lines for readability as the number of arguments grows @include position( fixed, $t: 30px, $r: 40px ); }
CSS
1
2
3
4
5
6
7
8
9
10
.absolute { position: absolute; bottom: 10px; right: 20px; } .fixed { position: fixed; top: 30px; right: 40px; }

# Variable arguments

Sometimes you don't know how many arguments you'll pass.
In that case, you can use variable arguments.
For variable arguments, add ... after the parameter.

SCSS
1
2
3
4
5
@mixin mixin-name($parameter...) { styles; } @include mixin-name(argumentA, argumentB, argumentC);
SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Takes arguments one at a time in order, and the 3rd parameter ($bg-values) takes any number of arguments @mixin bg($width, $height, $bg-values...) { width: $width; height: $height; background: $bg-values; } div { // Pass arguments in order to match the mixin (bg) above, and from the 3rd one on, pass as many as you like @include bg( 100px, 200px, url("/images/a.png") no-repeat 10px 20px, url("/images/b.png") no-repeat, url("/images/c.png") ); }
CSS
1
2
3
4
5
6
7
div { width: 100px; height: 200px; background: url("/images/a.png") no-repeat 10px 20px, url("/images/b.png") no-repeat, url("/images/c.png"); }

Above, we used ... on the parameter that receives the arguments to take variable arguments.
This time, let's do the opposite and use variable arguments for the values we pass.

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@mixin font( $style: normal, $weight: normal, $size: 16px, $family: sans-serif ) { font: { style: $style; weight: $weight; size: $size; family: $family; } } div { // Pass values matching the order and number of parameters $font-values: italic, bold, 16px, sans-serif; @include font($font-values...); } span { // Put only the needed values in a variable as keyword arguments and pass it $font-values: (style: italic, size: 22px); @include font($font-values...); } a { // Pass only the needed values as keyword arguments @include font((weight: 900, family: monospace)...); }
CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
div { font-style: italic; font-weight: bold; font-size: 16px; font-family: sans-serif; } span { font-style: italic; font-weight: normal; font-size: 22px; font-family: sans-serif; } a { font-style: normal; font-weight: 900; font-size: 16px; font-family: monospace; }

# @content

If a declared mixin contains @content, you can pass the style block you want into that spot.
This lets you add selectors, properties, and more to what an existing mixin already does.

SCSS
1
2
3
4
5
6
7
8
9
@mixin mixin-name() { styles; @content; } @include mixin-name() { // Style block styles; }

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@mixin icon($url) { &::after { content: $url; @content; } } .icon1 { // Use only what the icon mixin already does @include icon("/images/icon.png"); } .icon2 { // Use the icon mixin with an added style block @include icon("/images/icon.png") { position: absolute; }; }

Compiled to:

CSS
1
2
3
4
5
6
7
.icon1::after { content: "/images/icon.png"; } .icon2::after { content: "/images/icon.png"; position: absolute; }

A style block passed to a mixin is evaluated in the scope where the style block is defined, not in the mixin's scope.
In other words, the mixin's parameters aren't used inside the passed style block; names there are resolved to global values.
It's easier to understand if you think of global variables and local variables.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$color: red; @mixin colors($color: blue) { // Mixin scope @content; background-color: $color; border-color: $color; } div { @include colors() { // Scope where the style block is defined color: $color; } }

Compiled to:

CSS
1
2
3
4
5
div { color: red; background-color: blue; border-color: blue; }

A mixin can also pass arguments to the style block it receives.
Pass arguments with @content(), and write using after @include to receive them as parameters.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
@mixin media($breakpoint) { @media (min-width: $breakpoint) { @content($breakpoint); } } .box { @include media(768px) using ($width) { max-width: $width; } }

Compiled to:

CSS
1
2
3
4
5
@media (min-width: 768px) { .box { max-width: 768px; } }

# Extend

You'll often run into cases where one selector needs to have all the styles of another selector.
In that case, you can use the selector extend feature.
Let's look at the following example.

SCSS
1
@extend selector;

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
.btn { padding: 10px; margin: 10px; background: blue; } .btn-danger { @extend .btn; background: red; }

Compiled to:

CSS
1
2
3
4
5
6
7
8
.btn, .btn-danger { padding: 10px; margin: 10px; background: blue; } .btn-danger { background: red; }

Do you like the compiled result?
Looking at the result, a multiple selector separated by , was created.

In fact, with @extend, you need to think about the following issues.

  • Where will my current selector (.btn-danger in the example above) be attached?
  • Could it cause unwanted side effects?
  • How much CSS does this one extend generate?

In the end, extend can be harmless or even helpful, but it can have just as many side effects.
So extend isn't recommended; use the mixins we covered above instead.

If you really must use extend, use a placeholder selector instead of a regular selector.
A placeholder selector starts with % and isn't compiled to CSS on its own; it's output only where it's extended with @extend.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
%btn-base { padding: 10px; margin: 10px; } .btn { @extend %btn-base; background: blue; } .btn-danger { @extend %btn-base; background: red; }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
.btn-danger, .btn { padding: 10px; margin: 10px; } .btn { background: blue; } .btn-danger { background: red; }

Also, @extend can't extend selectors outside a conditional block such as @media.
If you need to reuse styles inside a media query, you have to use a mixin.

For more details on why it isn't recommended, see Sass Guidelines Extend.

# Functions

You can define and use your own functions.
Functions and mixins are very similar, but what they return is different.

As we saw above, a mixin returns the styles you specify,
while a function usually returns a specific computed value with the @return directive.

SCSS
1
2
3
4
5
6
7
8
9
// Mixins @mixin mixin-name($parameter) { styles; } // Functions @function function-name($parameter) { @return value }

How you use them is also different.
A mixin uses the @include directive,
while a function is used directly by its name.

SCSS
1
2
3
4
5
// Mixin @include mixin-name(argument); // Functions function-name(argument)

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@use "sass:math"; $max-width: 980px; @function columns($number: 1, $columns: 12) { @return math.div($max-width * $number, $columns); } .box_group { width: $max-width; .box1 { width: columns(); // 1 } .box2 { width: columns(8); } .box3 { width: columns(3); } }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.box_group { /* Total width */ width: 980px; } .box_group .box1 { /* About 8.3% of the total width */ width: 81.6666666667px; } .box_group .box2 { /* About 66.7% of the total width */ width: 653.3333333333px; } .box_group .box3 { /* 25% of the total width */ width: 245px; }

As shown above, functions are used without a separate directive like @include.
Built-in functions are called with a namespace, like math.div(), so they don't collide with functions you define.
However, they can collide with functions loaded without a namespace through @use ... as *.
If a function is shared across many files, it's better to give it a distinct name, like ui-columns() instead of columns().

# Conditionals and loops

# The if function

It returns only one of two expressions, depending on the value of the condition (true or false).
It's similar to the conditional (ternary) operator.

Now that CSS also has an if() function, Sass's if() uses the same syntax as CSS.
When you write a Sass condition inside sass(), the condition is checked at compile time: if it's true, the expression after : is returned, and if it's false, the expression after else: is returned.
If you write several sass(condition): expression; pairs, the expression of the first true condition is returned.

SCSS
1
if(sass(condition): expression1; else: expression2)

SCSS:

SCSS
1
2
3
4
$width: 555px; div { width: if(sass($width > 300px): $width; else: null); }

Compiled to:

CSS
1
2
3
div { width: 555px; }

The old if($width > 300px, $width, null) syntax has been deprecated since Dart Sass 1.95.0.
The new syntax is also only available from Dart Sass 1.95.0, so if your project uses an older version, upgrade the compiler first.

# @if

The @if directive lets you branch based on a condition, similar to if statements.
The directives you can use with it are @else and if.
With these extra directives, you can write more complex conditionals.

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @if @if (condition) { /* Statements when the condition is true */ } // @if @else @if (condition) { /* Statements when the condition is true */ } @else { /* Statements when the condition is false */ } // @if @else if @if (condition1) { /* Statements when condition1 is true */ } @else if (condition2) { /* Statements when condition2 is true */ } @else { /* Statements when all conditions are false */ }

You can leave out the () around the condition, so writing it without () may be more convenient.

SCSS
1
2
3
4
5
6
$bg: true; div { @if $bg { background: url("/images/a.jpg"); } }

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
$color: orange; div { @if $color == strawberry { color: #FE2E2E; } @else if $color == orange { color: #FE9A2E; } @else if $color == banana { color: #FFFF00; } @else { color: #2A1B0A; } }

Compiled to:

CSS
1
2
3
div { color: #FE9A2E; }

You can use the logical operators and, or, and not in conditions.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
@function limitSize($size) { @if $size >= 0 and $size <= 200px { @return 200px; } @else { @return 800px; } } div { width: limitSize(180px); height: limitSize(340px); }

Compiled to:

CSS
1
2
3
4
div { width: 200px; height: 800px; }

Let's look at another example!
The math.is-unitless() function from the sass:math module returns whether a number has no unit.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@use "sass:math"; @mixin pCenter($w, $h, $p: absolute) { @if $p == absolute or $p == fixed // or not $p == relative // or not $p == static { width: if(sass(math.is-unitless($w)): #{$w}px; else: $w); height: if(sass(math.is-unitless($h)): #{$h}px; else: $h); position: $p; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } } .box1 { @include pCenter(10px, 20px); } .box2 { @include pCenter(50, 50, fixed); } .box3 { @include pCenter(100, 200, relative); }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.box1 { width: 10px; height: 20px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } .box2 { width: 50px; height: 50px; position: fixed; top: 0; bottom: 0; left: 0; right: 0; margin: auto; }

# @for

@for outputs styles repeatedly.
It's similar to the for statement.

@for comes in two forms: one that uses through and one that uses to.
The two forms interpret the end value differently.

SCSS
1
2
3
4
5
6
7
8
9
10
11
// through // Repeats up to and including the end @for $variable from start through end { // Loop body } // to // Repeats until just before the end @for $variable from start to end { // Loop body }

Let's look at the following example to understand the difference.
By convention, $i is used as the variable.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
// Repeat 3 times, starting from 1 @for $i from 1 through 3 { .through:nth-child(#{$i}) { width : 20px * $i } } // Repeat from 1 until just before 3 (2 times) @for $i from 1 to 3 { .to:nth-child(#{$i}) { width : 20px * $i } }

Compiled to:

CSS
1
2
3
4
5
6
.through:nth-child(1) { width: 20px; } .through:nth-child(2) { width: 40px; } .through:nth-child(3) { width: 60px; } .to:nth-child(1) { width: 20px; } .to:nth-child(2) { width: 40px; }

to can be useful when you need to repeat only until just before the given value.
However, @for is especially useful with :nth-child(), so through is generally recommended.

# @each

@each is used to loop over List and Map data.
It's similar to the for...in statement.

SCSS
1
2
3
@each $variable in data { // Loop body }

Let's loop over List data.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
// List Data $fruits: (apple, melon, banana, mango); .fruits { @each $fruit in $fruits { li.#{$fruit} { background: url("/images/#{$fruit}.png"); } } }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
.fruits li.apple { background: url("/images/apple.png"); } .fruits li.melon { background: url("/images/melon.png"); } .fruits li.banana { background: url("/images/banana.png"); } .fruits li.mango { background: url("/images/mango.png"); }

If you need the index on each iteration, you can use the list.index() function from the sass:list module.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
@use "sass:list"; $fruits: (apple, melon, banana, mango); .fruits { @each $fruit in $fruits { $i: list.index($fruits, $fruit); li:nth-child(#{$i}) { left: 50px * $i; } } }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
.fruits li:nth-child(1) { left: 50px; } .fruits li:nth-child(2) { left: 100px; } .fruits li:nth-child(3) { left: 150px; } .fruits li:nth-child(4) { left: 200px; }

You can also loop over several Lists at the same time.
However, every List must have the same length.

SCSS:

SCSS
1
2
3
4
5
6
7
8
9
$apple: (apple, korea); $melon: (melon, china); $banana: (banana, japan); @each $fruit, $country in $apple, $melon, $banana { .box-#{$fruit} { background: url("/images/#{$country}.png"); } }

Compiled to:

CSS
1
2
3
4
5
6
7
8
9
.box-apple { background: url("/images/korea.png"); } .box-melon { background: url("/images/china.png"); } .box-banana { background: url("/images/japan.png"); }

When looping over Map data, you need two variables for each item.

SCSS
1
2
3
@each $key-variable, $value-variable in data { // Loop body }
SCSS
1
2
3
4
5
6
7
8
9
10
11
$fruits-data: ( apple: korea, melon: china, banana: japan ); @each $fruit, $country in $fruits-data { .box-#{$fruit} { background: url("/images/#{$country}.png"); } }
CSS
1
2
3
4
5
6
7
8
9
.box-apple { background: url("/images/korea.png"); } .box-melon { background: url("/images/china.png"); } .box-banana { background: url("/images/japan.png"); }

# @while

@while repeats its contents until the condition evaluates to false.
As with the while statement, a wrong condition can cause an infinite loop during compilation.
It isn't recommended.

SCSS
1
2
3
@while condition { // Loop body }
SCSS
1
2
3
4
5
6
7
8
$i: 6; @while $i > 0 { .item-#{$i} { width: 2px * $i; } $i: $i - 2; }
CSS
1
2
3
.item-6 { width: 12px; } .item-4 { width: 8px; } .item-2 { width: 4px; }

# Built-in modules

The built-in functions that Sass provides out of the box are organized into built-in modules.
To use them, first load the module with @use, then call the function with its namespace.

SCSS
1
2
3
4
5
@use "sass:math"; div { width: math.div(100px, 3); }

Global built-in functions that were called without a namespace, like map-get() and str-slice(), have been deprecated since Dart Sass 1.80.0 and are scheduled to be removed in Dart Sass 3.0.0.
All of these functions have moved into their modules, so use them like map.get() and string.slice().

Rather than covering all of them, I've listed only the functions that, in my own experience, you need or might need.
You can find every built-in function in Sass Built-in Modules.

# sass:math

Function Description
math.div($n1, $n2) Returns the result of dividing two numbers.
math.percentage($n) Converts a number (ignoring units) to a percentage.
math.round($n) Rounds to the nearest integer.
math.ceil($n) Rounds up to an integer.
math.floor($n) Rounds down to an integer (truncates).
math.abs($n) Returns the absolute value of a number.
math.min($n...) Finds the smallest of the numbers.
math.max($n...) Finds the largest of the numbers.
math.random([$limit]) Without an argument, returns a random number between 0 and 1; with one, returns an integer between 1 and $limit.
math.unit($n) Returns the unit of a number.
math.is-unitless($n) Returns whether a number has no unit.
math.compatible($n1, $n2) Returns whether two numbers can be used together in an operation.

# sass:string

Function Description
string.unquote($string) Removes quotes from a string.
string.quote($string) Adds quotes to a string.
string.length($string) Returns the length of a string.
string.insert($string, $insert, $index) Inserts a string at the index-th position of a string.
string.index($string, $substring) Returns the first index of a substring in a string.
string.slice($string, $start-at, [$end-at]) Extracts part of a string (from one character position to another).
string.split($string, $separator) Splits a string by a separator and returns a List.
string.to-upper-case($string) Converts a string to uppercase.
string.to-lower-case($string) Converts a string to lowercase.

# sass:color

Function Description
color.mix($color1, $color2, [$weight]) Mixes two colors.
color.adjust($color, $channel: $amount) Adds to or subtracts from channel values.
color.scale($color, $channel: $percent) Adjusts a channel by a percentage of its remaining range.
color.change($color, $channel: $value) Changes channel values to the given values.
color.channel($color, $channel, $space) Returns the value of a channel in a specific color space.
color.grayscale($color) Converts a color to gray.
color.invert($color) Inverts a color.
color.complement($color) Returns the complementary color.
color.space($color) Returns the name of the color space a color belongs to.
color.to-space($color, $space) Converts a color to another color space.

For $channel, you can use $red, $green, $blue, $hue, $saturation, $lightness, $alpha, and more.

# sass:list

All List built-in functions return a new List instead of updating the existing one.
All List built-in functions can also be used on Map data.

Function Description
list.length($list) Returns the number of items in a List.
list.nth($list, $n) Returns the nth value in a List.
list.set-nth($list, $n, $value) Replaces the nth value in a List with another value.
list.append($list, $value, [$separator]) Adds a value to a List.
list.join($list1, $list2, [$separator]) Combines two Lists into one.
list.zip($lists...) Combines several Lists into one multidimensional List.
list.index($list, $value) Returns the index of a specific value in a List.
list.separator($list) Returns the separator of a List.

# sass:map

All Map built-in functions return a new Map instead of updating the existing one.

Function Description
map.get($map, $key) Returns the value for a specific key in a Map.
map.set($map, $key, $value) Changes the value for a specific key in a Map.
map.merge($map1, $map2) Merges two Maps into a new Map.
map.remove($map, $keys...) Removes specific keys from a Map.
map.has-key($map, $key) Returns whether a Map has a specific key.
map.keys($map) Returns all keys in a Map as a List.
map.values($map) Returns all values in a Map as a List.

# sass:meta

Function Description
meta.variable-exists($name) Returns whether a variable exists in the current scope. (For the argument, use only the variable's name without $.)
meta.mixin-exists($name) Returns whether a mixin exists.
meta.function-exists($name) Returns whether a function exists.
meta.type-of($value) Returns the data type of a value.
meta.inspect($value) Returns a value as a human-readable string.
meta.load-css($url, [$with]) Outputs a module's CSS at the current location. It's a mixin, not a function, so use it like @include meta.load-css("theme").

# Migration

As we've seen in this post, Dart Sass has deprecated a number of older syntaxes.
Syntax that still works prints a warning every time you compile and will be removed in a scheduled version.

Old way Current way Deprecated Removal planned
@import "config" @use "config", @forward "config" 1.80.0 3.0.0
Global built-in functions like map-get($map, $key) Built-in module functions like map.get($map, $key) 1.80.0 3.0.0
Older color functions like lighten($color, 10%) color.adjust($color, $lightness: 10%) 1.79.0 2.0.0
$width / 2 math.div($width, 2), calc($width / 2) 1.33.0 2.0.0
if($cond, $a, $b) if(sass($cond): $a; else: $b) 1.95.0 TBD
!global on an undeclared variable Declare it at the top level first 1.17.2 2.0.0

If your project uses code like this, you can convert it all at once with Sass Migrator.

BASH
1
npm i -g sass-migrator
Install

module converts @import to @use and @forward and adds namespaces to global built-in functions.
Add the --migrate-deps option to also convert the files that the entry file loads.

BASH
1
sass-migrator module --migrate-deps scss/main.scss

division converts / division to math.div(), color converts older color functions like lighten() to color.adjust(), and if-function converts the old if() syntax to the CSS if() syntax.

BASH
1
2
3
sass-migrator division --migrate-deps scss/main.scss sass-migrator color --migrate-deps scss/main.scss sass-migrator if-function --migrate-deps scss/main.scss

The conversion can't be undone, so run it when you can review the changes with Git.