The Complete Guide to SCSS/Sass
# Overview
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:
12345678.list width: 100px float: left li color: red background: url("./image.jpg") &:last-child margin-right: -10px
SCSS:
1234567891011.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:
1234567=flex-center display: flex justify-content: center align-items: center .box +flex-center
SCSS:
1234567@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.
1npm i -D sass
You can compile a single file or an entire directory.
12345## 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.
1npx 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.
1npm 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.
1npm i -D style-loader css-loader sass-loader sass-embedded
1234567891011121314module.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.
1npm i -D parcel sass
1<link rel="stylesheet" href="./src/main.scss">
1npx parcel index.html
# Comments
A CSS comment is /* ... */.
Sass (SCSS) uses two styles of comments, like JavaScript.
12// 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:
12345678/* 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:
12345/* 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
nullis 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.
SCSS:
123456789.section { width: 100%; .list { padding: 20px; li { float: left; } } }
Compiled to:
123456789.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:
1234567891011121314.btn { position: absolute; &.active { color: red; } } .list { li { &:last-child { margin-right: 0; } } }
Compiled to:
123456789.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:
12345.fs { &-small { font-size: 12px; } &-medium { font-size: 14px; } &-large { font-size: 16px; } }
Compiled to:
123456789.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:
123456789101112.list { $w: 100px; $h: 50px; li { width: $w; height: $h; } @at-root .box { width: $w; height: $h; } }
Compiled to:
12345678.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.)
1234567891011121314.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:
123456789101112131415.box { font: { weight: bold; size: 10px; family: sans-serif; }; margin: { top: 10px; left: 20px; }; padding: { bottom: 40px; right: 30px; }; }
Compiled to:
123456789.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.
1$variable-name: value;
SCSS:
123456789$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:
12345.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.
12345678.box1 { $color: #111; background: $color; } // Error .box2 { background: $color; }
# Variable reassignment
You can reassign a variable to another variable like this.
SCSS:
12345678910$red: #FF0000; $blue: #0000FF; $color-primary: $blue; $color-danger: $red; .box { color: $color-primary; background: $color-danger; }
Compiled to:
1234.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:
123456789101112$color: #000; .box1 { $color: #111 !global; background: $color; } .box2 { background: $color; } .box3 { $color: #222; background: $color; }
Compiled to:
123456789.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:
123456$color-primary: red; .box { $color-primary: blue !default; background: $color-primary; }
Compiled to:
123.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.
12$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.
123456$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.
1234@use "sass:string"; $family: string.unquote("Noto+Sans+KR"); @import url("https://fonts.googleapis.com/css2?family=#{$family}");
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.
# 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.
1234567Sass-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.
12@use "header"; @use "side-menu";
Then compile these files into the css/ directory.
1npx 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.
123456789Sass-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.
12345678$primary: royalblue; $radius: 4px; @mixin flex-center { display: flex; justify-content: center; align-items: center; }
1234567@use "config"; .btn { background: config.$primary; border-radius: config.$radius; @include config.flex-center; }
You can rename the namespace with as.
12345@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.
12345@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.
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.
12$-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.
12$primary: royalblue !default; $radius: 4px !default;
123456789@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.
123@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.
123456@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.
123@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.
12// 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://orhttps:// - When it's written with
url() - When it has a media query
1234@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.
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.
12width: 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:
123456789@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:
1234567div { 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().
123div { width: calc(var(--size) / 2); }
123div { 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:
1234div::after { content: "Hello " + World; flex-flow: row + "-reverse" + " " + wrap }
Compiled to:
1234div::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.
1234div { // 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:
12345678910@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:
123456div { 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:
123456$width: 90px; div { @if not ($width > 100px) { height: 300px; } }
Compiled to:
123div { 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:
123@mixin mixin-name { styles; }
Sass:
12=mixin-name styles
SCSS:
123456@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).
12345678910111213141516@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:
1@include mixin-name;
Sass:
1+mixin-name
SCSS:
123456h1 { @include large-text; } div { @include large-text; }
Compiled to:
12345678910111213141516171819202122232425h1 { 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:
1234@mixin mixin-name($parameter) { styles; } @include mixin-name(argument);
Sass:
1234=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:
123456@mixin dash-line($width, $color) { border: $width dashed $color; } .box1 { @include dash-line(1px, red); } .box2 { @include dash-line(4px, blue); }
Compiled to:
123456.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.
123@mixin mixin-name($parameter: default-value) { styles; }
SCSS:
123456@mixin dash-line($width: 1px, $color: black) { border: $width dashed $color; } .box1 { @include dash-line; } .box2 { @include dash-line(4px); }
Compiled to:
123456.box1 { border: 1px dashed black; } .box2 { border: 4px dashed black; }
# Keyword arguments
12345@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.
1234567891011121314151617181920212223242526@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 ); }
12345678910.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.
12345@mixin mixin-name($parameter...) { styles; } @include mixin-name(argumentA, argumentB, argumentC);
1234567891011121314151617// 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") ); }
1234567div { 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.
123456789101112131415161718192021222324252627@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)...); }
123456789101112131415161718div { 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.
123456789@mixin mixin-name() { styles; @content; } @include mixin-name() { // Style block styles; }
SCSS:
12345678910111213141516@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:
1234567.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:
123456789101112131415$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:
12345div { 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:
1234567891011@mixin media($breakpoint) { @media (min-width: $breakpoint) { @content($breakpoint); } } .box { @include media(768px) using ($width) { max-width: $width; } }
Compiled to:
12345@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.
1@extend selector;
SCSS:
123456789.btn { padding: 10px; margin: 10px; background: blue; } .btn-danger { @extend .btn; background: red; }
Compiled to:
12345678.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-dangerin 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:
12345678910111213%btn-base { padding: 10px; margin: 10px; } .btn { @extend %btn-base; background: blue; } .btn-danger { @extend %btn-base; background: red; }
Compiled to:
123456789101112.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.
123456789// 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.
12345// Mixin @include mixin-name(argument); // Functions function-name(argument)
SCSS:
123456789101112131415161718192021@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:
12345678910111213141516.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.
1if(sass(condition): expression1; else: expression2)
SCSS:
1234$width: 555px; div { width: if(sass($width > 300px): $width; else: null); }
Compiled to:
123div { width: 555px; }
# @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.
1234567891011121314151617181920// @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.
123456$bg: true; div { @if $bg { background: url("/images/a.jpg"); } }
SCSS:
123456789101112$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:
123div { color: #FE9A2E; }
You can use the logical operators and, or, and not in conditions.
SCSS:
123456789101112@function limitSize($size) { @if $size >= 0 and $size <= 200px { @return 200px; } @else { @return 800px; } } div { width: limitSize(180px); height: limitSize(340px); }
Compiled to:
1234div { 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:
1234567891011121314151617181920212223242526272829@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:
123456789101112131415161718192021.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.
1234567891011// 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:
12345678910111213// 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:
123456.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.
123@each $variable in data { // Loop body }
Let's loop over List data.
SCSS:
12345678910// List Data $fruits: (apple, melon, banana, mango); .fruits { @each $fruit in $fruits { li.#{$fruit} { background: url("/images/#{$fruit}.png"); } } }
Compiled to:
123456789101112.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:
123456789101112@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:
123456789101112.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:
123456789$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:
123456789.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.
123@each $key-variable, $value-variable in data { // Loop body }
1234567891011$fruits-data: ( apple: korea, melon: china, banana: japan ); @each $fruit, $country in $fruits-data { .box-#{$fruit} { background: url("/images/#{$country}.png"); } }
123456789.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.
123@while condition { // Loop body }
12345678$i: 6; @while $i > 0 { .item-#{$i} { width: 2px * $i; } $i: $i - 2; }
123.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.
12345@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.
[]marks an optional argument.- Zero-based numbering is not used.
# 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.
1npm i -g sass-migrator
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.
1sass-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.
123sass-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.
Thanks for reading to the end.
Likes and kind comments help keep this blog going!