Sass Basics
Variables & Nesting
Sass variables use $ prefix. Nesting mirrors HTML structure. & refers to the parent selector. Use lighten()/darken() for color manipulation.
// SCSS syntax ($var)
$primary-color: #3498db;
$font-stack: -apple-system, sans-serif;
body {
font-family: $font-stack;
color: $primary-color;
a {
color: lighten($primary-color, 20%);
&:hover {
color: darken($primary-color, 10%);
}
}
}SCSS vs Indented Syntax
SCSS (.scss) is a superset of CSS — every valid CSS file is valid SCSS. Sass (.sass) removes braces and semicolons, relying on indentation. SCSS is more popular and easier to migrate. Choose one and be consistent within a project.
// SCSS (.scss) - uses braces and semicolons
.box {
width: 100px;
height: 100px;
}
// Sass (.sass) - indentation-based, no braces/semicolons
.box
width: 100px
height: 100px
// Both compile to the same CSS
// .box { width: 100px; height: 100px; }Comments
// comments are silent (removed in output). /* */ comments are preserved in compiled CSS. /*! */ comments are kept even in compressed/production mode — use for licenses or critical notices. Use // for internal documentation.
// Single-line comment - NOT compiled to CSS
/* Multi-line comment - compiled to CSS */
/*! Important comment - always kept even in compressed mode */
// Silent comment for developers
$base-font: 16px;
/*! Keep this copyright notice */
body { font-size: $base-font; }Compilation
Sass compiles .scss/.sass to .css. --watch auto-recompiles on save. Output styles: expanded (default readable), nested (indented), compact (one rule per line), compressed (minified for production). Use compressed in production to reduce file size.
// Compile a single file
// $ sass input.scss output.css
// Watch for changes
// $ sass --watch input.scss:output.css
// Watch a directory
// $ sass --watch scss/:css/
// Compressed output
// $ sass --style=compressed input.scss output.css
// Four output styles: expanded, nested, compact, compressed
$width: 100px;
.box { width: $width; }Selector Inheritance Overview
Sass is a CSS preprocessor adding variables, nesting, mixins, inheritance, functions, and control flow. These reduce repetition (DRY principle) and make large stylesheets manageable. All Sass features compile down to standard CSS.
// Sass adds programming features to CSS:
// 1. Variables ($var)
// 2. Nesting
// 3. Mixins (@mixin / @include)
// 4. Inheritance (@extend)
// 5. Functions (@function)
// 6. Control directives (@if, @for, @each, @while)
// 7. Partials & Modules (@use, @forward)
// These features help keep stylesheets DRY and maintainable.
$radius: 4px;
.card { border-radius: $radius; }Variables & Data Types
Variable Declaration & Scope
Variables declared outside any block are global. Variables declared inside a block are local to that block. Locals shadow globals with the same name. Use locals to avoid polluting the global namespace.
$color: #333; // global scope
$padding: 10px; // global
.block {
$local-color: #fff; // local to .block
color: $local-color;
padding: $padding; // can read globals
}
// $local-color is NOT available here
// .other { color: $local-color; } // ERROR!default Flag
!default assigns a value only if the variable is null or undefined. This lets users override library defaults by setting variables BEFORE importing the library. Critical for building configurable Sass libraries and design systems.
// Only assign if the variable is not already defined
$base-color: #333 !default;
$base-color: #fff; // this wins (already? no - assigned above first)
// Useful in libraries/partials for user overrides
// _theme.scss
$primary: blue !default;
.button { background: $primary; }
// User config BEFORE importing
// $primary: red;
// @use 'theme'; -> button is red!global Flag
!global explicitly assigns to the global scope from within a block (mixin, function, control directive). Since Sass 3+, assignment inside blocks is local by default. Use !global sparingly — global mutation can make code hard to reason about.
$count: 0;
@for $i from 1 through 3 {
$count: $count + $i !global;
}
// $count is now 6 (1+2+3)
.total { content: "#{$count}"; }
// Without !global, the assignment inside @for
// would create a local variable insteadData Types
Sass has 7 data types: numbers (with units), strings, colors, booleans, null, lists, and maps. type-of() returns the type as a string. Understanding types is essential for using functions and control directives correctly.
// Sass has 7 data types:
$number: 16px; // number (with optional unit)
$string: "Helvetica"; // string (quoted or unquoted)
$color: #3498db; // color
$boolean: true; // boolean (true/false)
$null: null; // null (no value)
$list: 10px 20px 30px; // list (space or comma separated)
$map: (key: value); // map (key-value pairs)
// Type checking
type-of($number) // number
type-of($list) // list
type-of($map) // mapInterpolation #{}
#{} interpolation injects variables into selectors, property names, strings, and other places where plain variables aren't allowed. Unlike $var which only works in values, #{} works almost anywhere. It converts any value to a string representation.
$name: "user";
$version: 2;
// Use #{} to insert variables into selectors and strings
.#{$name}-avatar { background: url("/img/#{$name}.png"); }
// In property names and values
.icon-#{$name} {
content: "v#{$version}";
}
// Interpolation in @media and @include
@media (max-width: #{$version * 100}px) {
body { font-size: 14px; }
}Nesting
Basic Nesting
Nesting lets you write selectors that mirror HTML hierarchy, avoiding repetition of parent selectors. The compiled CSS creates descendant selectors. This improves readability but overuse creates overly specific selectors — keep nesting to 3-4 levels max.
// SCSS nesting mirrors HTML structure
.nav {
background: #333;
ul {
list-style: none;
margin: 0;
li {
display: inline-block;
}
}
a {
color: white;
text-decoration: none;
}
}
// Compiles to: .nav {...} .nav ul {...} .nav ul li {...} .nav a {...}Deep Nesting (Anti-Pattern)
Deep nesting creates long, overly specific selectors that are hard to override and slower for browsers to match. The rule of thumb: don't nest more than 3-4 levels. Flatten deep structures with explicit BEM-style classes for maintainability.
// AVOID: overly deep nesting
.header {
.nav {
> ul {
li {
a {
span { // 5 levels deep!
color: red;
}
}
}
}
}
}
// Compiles to: .header .nav > ul li a span
// Too specific - hard to override, slow to match
// BETTER: flatten with classes
.header-nav-link-text { color: red; }Nested Properties
Nested properties use a colon after the property namespace (font:, border:, margin:) to group related sub-properties. This avoids repeating the prefix and is purely syntactic sugar. It compiles to standard CSS properties like font-family, border-top, etc.
.icon {
// Shorthand for font-family, font-size, font-weight...
font: {
family: "Arial";
size: 14px;
weight: bold;
}
// border-top, border-right, etc.
border: {
top: 1px solid #ccc;
bottom: 2px dashed #999;
}
}
// Compiles to: font-family, font-size, font-weight, border-top, border-bottomNesting Media Queries
Nesting @media queries inside rules keeps responsive styles co-located with the component, improving maintainability. The compiled CSS hoists @media to the top level. This is a major readability win over scattering media queries across files.
.container {
width: 100%;
// Media query nested inside the rule
@media (min-width: 768px) {
width: 720px;
}
@media (min-width: 1024px) {
width: 960px;
}
}
// Compiles to:
// .container { width: 100%; }
// @media (min-width: 768px) { .container { width: 720px; } }
// @media (min-width: 1024px) { .container { width: 960px; } }Selector Lists in Nesting
When you nest a comma-separated selector list, Sass combines each item with the parent. This is cleaner than writing each combination manually. The & can also be used within nested lists to append classes/modifiers to multiple parents at once.
.btn {
// Comma creates a selector list
.icon,
.label {
color: white;
}
}
// Compiles to:
// .btn .icon, .btn .label { color: white; }
// Nesting a list with &
.card,
.panel {
&.active { border-color: blue; }
}
// .card.active, .panel.active { border-color: blue; }Parent Selector (&)
Basic Parent Selector
& refers to the parent selector, allowing pseudo-classes (:hover, :focus), pseudo-elements (::before), and modifiers to be nested. This keeps related styles together. &:hover compiles to .button:hover. Without &, you'd get a descendant selector.
.button {
background: blue;
color: white;
// & refers to the parent (.button)
&:hover {
background: darkblue;
}
&:focus {
outline: 2px solid blue;
}
&::after {
content: "→";
}
}
// Compiles to: .button, .button:hover, .button:focus, .button::afterBEM with &
& enables BEM (Block Element Modifier) naming concisely. &__title appends __title to the parent, creating .card__title. &--featured creates modifier classes. This keeps BEM class generation DRY and consistent within a block definition.
// BEM methodology with parent selector
.card {
background: white;
&__title { // .card__title
font-size: 18px;
}
&__body { // .card__body
padding: 16px;
}
&--featured { // .card--featured
border-color: gold;
}
&--featured &__title { // .card--featured .card__title
color: gold;
}
}Suffixing Parent Selector
Placing & at the start of a nested selector appends the suffix to the parent, generating compound selectors like .button-primary. This is useful for naming variants. Be careful — overusing suffixing can obscure the generated class names when reading the source.
.button {
background: blue;
// Append suffix to parent
&-primary { background: darkblue; } // .button-primary
&-secondary { background: gray; } // .button-secondary
&-danger { background: red; } // .button-danger
// Combining with state
&-primary.is-active { font-weight: bold; }
}
// Generates: .button, .button-primary, .button-secondary, .button-dangerReversed Nesting
Placing & at the end of a nested selector reverses the order — the parent becomes a descendant. This is perfect for contextual/theming styles like .dark-theme .modal where the component adapts to its ancestor context, keeping context-specific overrides near the component.
.modal {
// & at the end - parent as descendant
.dark-theme & {
background: #222;
color: #eee;
}
.sidebar & {
width: 200px;
}
}
// Compiles to:
// .dark-theme .modal { background: #222; color: #eee; }
// .sidebar .modal { width: 200px; }
// Useful when parent is styled differently in a contextMultiple & in One Selector
You can use & multiple times in a single selector. & + & generates .button + .button for sibling styling. This is powerful for spacing between repeated elements and complex state-based selectors. Each & is replaced with the full parent selector.
.button {
// Multiple & references in one selector
& + & { // .button + .button (adjacent sibling)
margin-left: 8px;
}
& ~ & { // .button ~ .button (general sibling)
margin-top: 4px;
}
&.active &__icon { // .button.active .button__icon
color: white;
}
.parent &.is-open &__content {
display: block;
}
}Partials (_partial files)
Creating Partials
Partials are Sass files named with a leading underscore (e.g. _variables.scss). The underscore tells Sass not to compile them into a separate CSS file. Partials are meant to be imported into a main file. This modularizes code for maintainability.
// _variables.scss - partial file (underscore prefix)
$primary: #3498db;
$secondary: #2ecc71;
$font-base: 16px;
// _buttons.scss
.button {
background: $primary;
padding: $font-base / 2;
}
// Partials are NOT compiled to their own CSS file.
// The underscore tells Sass this is a partial.
// Import them into a main file:
// main.scss: @use 'variables'; @use 'buttons';Partial Naming Conventions
Partials use descriptive names with a leading underscore. The @use or @import statement omits the underscore and extension — Sass resolves 'variables' to _variables.scss automatically. Consistent naming helps teams locate code quickly.
// Common partial naming patterns:
// _variables.scss - variables
// _mixins.scss - mixins
// _functions.scss - functions
// _reset.scss - CSS reset
// _base.scss - base styles
// _typography.scss - typography
// _buttons.scss - button component
// _header.scss - header component
// _footer.scss - footer component
// _mediaqueries.scss - media queries
// Import without the underscore or extension:
// @use 'variables';
// @use 'mixins';7-1 Architecture Pattern
The 7-1 pattern is a widely-adopted Sass architecture: 7 folders (abstracts, base, components, layout, pages, themes, vendors) plus one main.scss entry point. This scales well for large projects by separating concerns. Each folder holds partials imported into main.scss.
// 7-1 architecture: 7 folders, 1 main file
// sass/
// abstracts/ - variables, functions, mixins
// base/ - reset, typography, base styles
// components/ - buttons, cards, nav
// layout/ - header, footer, grid
// pages/ - home, about specific styles
// themes/ - theme definitions
// vendors/ - third-party
// main.scss - imports everything
// main.scss
// @use 'abstracts/variables';
// @use 'base/reset';
// @use 'components/buttons';
// @use 'layout/header';Index Partials (_index.scss)
An _index.scss (or _all.scss) partial acts as a folder's public API, forwarding all its partials. This lets you import an entire folder with one @use statement. When you add a new partial, update only the index — the main file stays unchanged. This is the Sass module system pattern.
// sass/components/_index.scss
// Forward all component partials from one entry point
@forward 'buttons';
@forward 'cards';
@forward 'nav';
@forward 'modal';
// Now in main.scss, one line imports everything:
// @use 'components';
// (resolves to components/_index.scss)
// This keeps the main file clean and lets
// you reorganize partials without touching main.scssPartial Best Practices
Best practices: one concern per partial, keep files small (50-200 lines), use @use for explicit dependency management, and avoid circular imports. Partials improve maintainability and reuse. The @use statement makes dependencies explicit, unlike the older @import.
// 1. One concern per partial
// _buttons.scss -> only buttons
// _forms.scss -> only forms
// 2. Keep partials small and focused
// (50-200 lines is a good target)
// 3. Use @use (not @import) for explicit dependencies
// _card.scss
@use '../abstracts/variables' as *;
@use '../abstracts/mixins' as *;
.card {
background: $white;
@include card-shadow;
}
// 4. Avoid circular dependencies@import & @use
@import (Deprecated)
@import is deprecated because it pollutes the global namespace, can execute files multiple times, and offers no privacy or namespacing. Sass recommends migrating to @use and @forward. Existing @import code still works but should be migrated for new projects.
// @import - the OLD way (deprecated in Sass 1.80+)
@import 'variables';
@import 'mixins';
@import 'buttons';
// Problems with @import:
// 1. Globals leak across files (no namespacing)
// 2. Each @import of the same file runs it again
// 3. No way to make members private
// 4. Extends and mixins can conflict
// Use @use instead (the modern module system)
$primary: blue;@use Basic (Namespaced)
@use loads a module once and exposes its members through a namespace (the filename by default). Access members with namespace.$var or namespace.mixin-name(). This prevents global pollution and naming conflicts. Each module is loaded only once, regardless of how many times @use appears.
// _variables.scss
$primary: #3498db;
$radius: 4px;
// main.scss - @use creates a namespace (filename by default)
@use 'variables';
.button {
// Access via namespace::member
background: variables.$primary;
border-radius: variables.$radius;
}
// The namespace prevents naming collisions
// and makes dependencies explicit@use with as
'as v' renames the namespace to something shorter. 'as *' loads members into the current scope without a prefix — convenient but risks collisions, so use sparingly. Named namespaces are safer and make it clear where each member comes from when reading the code.
// _variables.scss
$primary: blue;
$secondary: green;
// Rename the namespace with 'as'
@use 'variables' as v;
.btn {
background: v.$primary;
color: v.$secondary;
}
// Or remove the namespace entirely (use with caution)
@use 'variables' as *;
.card {
background: $primary; // direct access, no prefix
}
// 'as *' can cause collisions - prefer named namespaces@use with Configuration (with)
'with (...)' configures a module's !default variables at load time. This replaces @import's global-variable configuration pattern. The with clause must come before any other use of the module. This is the standard way to configure library themes and design tokens.
// _theme.scss
$primary: blue !default;
$radius: 4px !default;
.button {
background: $primary;
border-radius: $radius;
}
// main.scss - configure variables on import
@use 'theme' with (
$primary: red,
$radius: 8px
);
// The 'with' clause sets variables marked !default
// This is how libraries are configured by users@use vs @import Comparison
Migration from @import to @use: members become namespaced, files load once, and members starting with - or _ are private (not exposed). @use makes dependencies explicit and prevents accidental global pollution. Use the sass-migrator tool to automate migration of large codebases.
// @import (old) - global, no namespace
@import 'variables';
.button { background: $primary; } // global $primary
// @use (new) - namespaced, loaded once
@use 'variables';
.button { background: variables.$primary; }
// Key differences:
// @import: global scope, runs multiple times, no privacy
// @use: namespaced, runs once, supports private (-prefix)
// _mixins.scss with a private member
@mixin -internal { /* private - not exposed */ }
@mixin public { @include -internal; }
// @use only exposes non-private (no dash prefix) membersMixins (@mixin / @include)
Defining & Using Mixins
@mixin defines a reusable block of declarations (and even rules). @include inserts the mixin's contents where it's used. Mixins are ideal for vendor prefixes, clearfixes, and repeated declaration groups. Unlike @extend, mixins duplicate the declarations each time they're included.
// Define a mixin with @mixin
@mixin center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin card-shadow {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
// Use with @include
.hero {
@include center;
height: 200px;
}
.modal {
@include center;
@include card-shadow;
}Mixin Arguments
Mixins accept arguments like functions. Pass by position (order matters) or by name (order-independent, clearer). Named arguments are useful for mixins with many optional parameters. Arguments make mixins flexible and reusable across different values.
@mixin button($bg, $color) {
background: $bg;
color: $color;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
// Pass arguments by position
.btn-primary { @include button(blue, white); }
.btn-danger { @include button(red, white); }
// Pass arguments by name (clearer, order-independent)
.btn-info { @include button($color: white, $bg: teal); }Default Arguments
Default argument values make mixins flexible — callers can omit arguments to use defaults. Use named arguments to override specific parameters while keeping others at their defaults. Default values are evaluated each time the mixin is included, not once at definition.
@mixin border($width: 1px, $style: solid, $color: #ccc) {
border: $width $style $color;
}
// Use all defaults
.box { @include border; }
// border: 1px solid #ccc;
// Override one - use named args to skip earlier ones
.card { @include border($color: blue); }
// border: 1px solid blue;
// Override by position
.banner { @include border(2px, dashed); }
// border: 2px dashed #ccc;Mixin with @content
@content injects a block of styles passed to @include. This is extremely powerful for media query mixins, keyframes, and wrapper contexts. The included block is evaluated in the calling scope, so it can use the caller's variables. See the @content section for argument passing.
// @content lets you pass a block of styles to a mixin
@mixin respond-to($breakpoint) {
@media (min-width: $breakpoint) {
@content; // injected styles go here
}
}
.sidebar {
width: 100%;
@include respond-to(768px) {
width: 250px; // this block replaces @content
float: left;
}
}
// Compiles to:
// .sidebar { width: 100%; }
// @media (min-width: 768px) { .sidebar { width: 250px; float: left; } }Variable Arguments (...)
The ... operator serves two purposes: in a mixin definition, it collects extra arguments into a list (variadic); at the call site, it expands a list into individual arguments. This is essential for mixins that wrap CSS properties accepting variable numbers of values, like box-shadow and transition.