Skip to content

Sass Hoja de referencia

Powerful CSS preprocessor with variables, nesting, and mixins.

01

Sass Basics

Variables & Nesting

Sass variables use $ prefix. Nesting mirrors HTML structure. & refers to the parent selector. Use lighten()/darken() for color manipulation.

sass
// 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.

sass
// 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.

sass
// 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.

sass
// 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
// 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; }
02

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.

sass
$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.

sass
// 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.

sass
$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 instead

Data 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
// 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)     // map

Interpolation #{}

#{} 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.

sass
$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; }
}
03

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.

sass
// 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.

sass
// 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.

sass
.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-bottom

Nesting 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.

sass
.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.

sass
.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; }
04

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.

sass
.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::after

BEM 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.

sass
// 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.

sass
.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-danger

Reversed 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.

sass
.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 context

Multiple & 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.

sass
.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;
  }
}
05

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.

sass
// _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.

sass
// 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.

sass
// 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
// 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.scss

Partial 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.

sass
// 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
06

@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.

sass
// @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.

sass
// _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.

sass
// _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.

sass
// _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.

sass
// @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) members
07

Mixins (@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.

sass
// 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.

sass
@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.

sass
@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.

sass
// @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.

sass
// ... captures multiple args into a list (or expands a list)
@mixin box-shadow($shadows...) {
  box-shadow: $shadows;
}

.card { @include box-shadow(0 1px 2px black, 0 4px 8px gray); }
// box-shadow: 0 1px 2px black, 0 4px 8px gray;

// Expanding a list into arguments
$shadows: 0 1px 2px black, 0 4px 8px gray;
.modal { @include box-shadow($shadows...); }

// Also works for @include with maps as keyword args
@mixin theme($colors) { /* ... */ }
@include theme(("bg": red, "fg": white)...);
08

@extend & Inheritance

Basic @extend

@extend makes one selector inherit all the styles of another, sharing the rule rather than duplicating it. The compiled CSS groups the selectors together. @extend is more DRY than a mixin for shared base styles, but it can create surprising selector chains — use carefully.

sass
.message {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

// .success inherits all .message styles
.success {
  @extend .message;
  background: #d4edda;
  border-color: #c3e6cb;
}

.error {
  @extend .message;
  background: #f8d7da;
  border-color: #f5c6cb;
}

// Compiles to: .message, .success, .error { padding: 10px; ... }

Extending Complex Selectors

@extend works with any selector — classes, element selectors, pseudo-classes, compound selectors. Extending .icon.large inherits rules matching both .icon and .icon.large. Be cautious extending complex selectors; the generated CSS can cascade in unexpected ways and bloat output.

sass
// You can extend any selector, not just classes
a:hover {
  text-decoration: underline;
}

.link-card:hover {
  @extend a:hover;  // inherits :hover rule
  color: blue;
}

// Extending with compound selectors
.icon {
  display: inline-block;
}

.icon.large {
  font-size: 24px;
}

.icon-x { @extend .icon.large; }  // extends BOTH rules

Chaining @extend

@extend chains: if B extends A and C extends B, C inherits from both. The compiled CSS groups selectors accordingly. Deep chains can produce large selector lists and surprising specificity. For most reuse scenarios, a mixin is more predictable than chained @extends.

sass
.base {
  display: block;
  padding: 8px;
}

.intermediate {
  @extend .base;
  background: #eee;
}

.final {
  @extend .intermediate;
  color: red;
}

// .final inherits from .intermediate which inherits from .base
// Compiles to: .base, .intermediate, .final { display: block; padding: 8px; }
//              .intermediate, .final { background: #eee; }
//              .final { color: red; }

@extend !optional

Adding !optional to @extend silently skips the extension if the target selector doesn't exist, instead of throwing an error. This is useful when extending optional or conditionally-loaded selectors. Without !optional, @extend fails the compilation if the target is missing.

sass
// !optional prevents errors if the selector doesn't exist
.fluent {
  @extend .nonexistent !optional;  // no error, just skipped
  color: blue;
}

// Without !optional, extending a missing selector throws:
// ".nonexistent failed to @extend"
// Use !optional when the extension target may or may not exist
// (e.g., optional theme classes from a third-party library)

@extend vs @include Mixin

@extend groups selectors (smaller CSS when reused heavily) but creates hidden coupling. @include mixins duplicate declarations (larger CSS) but are self-contained and configurable. Prefer mixins for new code — they're predictable. Use @extend mainly for semantic relationships within one file.

sass
// @extend - shares one rule, groups selectors
.btn { padding: 8px; }
.primary { @extend .btn; background: blue; }
// -> .btn, .primary { padding: 8px; }

// @mixin - duplicates declarations each use
@mixin btn { padding: 8px; }
.primary { @include btn; background: blue; }
// -> .primary { padding: 8px; background: blue; }

// When to use which:
// @extend: share base styles semantically (DRY selectors)
// @mixin: configurable repeated declarations (with args)
// Avoid @extend across module boundaries (can cause side effects)
09

Placeholder Selectors (%placeholder)

%placeholder Basics

%placeholder selectors (e.g. %base-message) are rules that only exist to be extended — they're never output to CSS on their own. This avoids the unused-base-class problem that @extend on real classes can cause. Placeholders are the cleanest way to share styles intended solely for inheritance.

sass
// %placeholder selectors are NOT output in CSS
// They only appear when @extended
%base-message {
  padding: 10px;
  border-radius: 4px;
}

.success { @extend %base-message; background: green; }
.error { @extend %base-message; background: red; }

// Compiles to (notice %base-message is gone):
// .success, .error { padding: 10px; border-radius: 4px; }
// .success { background: green; }
// .error { background: red; }

Placeholder vs Class

When you @extend a real class, that class appears in the output even if never used in HTML. Placeholders solve this: %base is only emitted where extended. Use real classes when the base is also used directly in HTML; use placeholders when the base is purely for inheritance.

sass
// Class as extend target - the class IS output
.message { padding: 10px; }  // appears in CSS even if unused
.success { @extend .message; }

// Placeholder as extend target - NOT output
%message { padding: 10px; }  // never appears in CSS
.success { @extend %message; }

// Result is identical for .success, but the placeholder
// version doesn't pollute the CSS with an unused .message rule.
// Use placeholders for internal/inheritance-only base styles.

Placeholder with @extend

A selector can @extend multiple placeholders, combining shared style sets. This composes reusable building blocks without outputting unused base classes. Placeholders shine for design-system primitives (cards, buttons, inputs) shared across many concrete components.

sass
%card-base {
  background: white;
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 16px;
}

%card-shadow {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

// Combine multiple placeholders
.featured-card {
  @extend %card-base;
  @extend %card-shadow;
  border-color: gold;
}

Multiple Placeholders Sharing

When multiple selectors @extend the same placeholder, Sass groups them together in the output, producing compact CSS. This is the most efficient form of style sharing in Sass. Placeholders enable a form of inheritance/composition without runtime CSS class bloat.

sass
%button { padding: 8px 16px; border: none; cursor: pointer; }
%rounded { border-radius: 4px; }
%shadow { box-shadow: 0 1px 3px rgba(0,0,0,0.2); }

// Many components share the same placeholders
.btn-primary { @extend %button; @extend %rounded; @extend %shadow; background: blue; }
.btn-secondary { @extend %button; @extend %rounded; background: gray; }
.btn-ghost { @extend %button; background: transparent; }

// Sass deduplicates: %button's rule is shared across all three

Placeholder Use Cases

Placeholders are ideal for reusable utility patterns: centering, clearfixes, accessibility (visually-hidden), and design-system primitives. They keep these patterns in one place and avoid emitting them unless used. This is a clean way to share non-configurable utility styles.

sass
// 1. Design system primitives
%abs-center {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}

// 2. Clearfix
%clearfix::after {
  content: "";
  display: table;
  clear: both;
}

// 3. Visually hidden (accessibility)
%visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

.modal { @extend %abs-center; }
.container { @extend %clearfix; }
.sr-only { @extend %visually-hidden; }
10

Functions (@function)

Defining Functions

@function defines a reusable computation that @return a value. Functions are called in property values (unlike mixins which output declarations). Use functions for calculations like unit conversions, color math, and responsive scaling. The return value can be any Sass data type.

sass
// @function defines a function that returns a value
@function rem($px) {
  $base: 16px;
  @return ($px / $base) * 1rem;
}

// Use it like a CSS function
body { font-size: rem(16); }      // 1rem
h1 { font-size: rem(32); }        // 2rem
.small { font-size: rem(12); }    // 0.75rem

// Functions return values; mixins output CSS rules

Function Arguments & Defaults

Functions support default arguments, named arguments, and variadic arguments (...), just like mixins. Defaults make functions convenient to call. Named arguments improve readability for functions with many optional parameters. Variadic args let functions accept any number of values.

sass
@function spacing($factor: 1, $base: 8px) {
  @return $base * $factor;
}

// Use defaults
.card { padding: spacing(); }       // 8px
.card-lg { padding: spacing(2); }   // 16px

// Override by name
.section { padding: spacing($base: 10px); }  // 10px
.hero { padding: spacing(3, 10px); }         // 30px

// Variable arguments work too
@function max-of($nums...) {
  @return ...; // implement with reduce
}

Built-in Functions Overview

Sass ships with rich built-in functions for colors, math, strings, lists, and maps. Modern Sass organizes these into modules (sass:math, sass:color, sass:string, sass:list, sass:map, sass:meta) loaded via @use. The global function names still work but are deprecated in favor of namespaced module functions.

sass
// Color functions
lighten(#000, 50%)       // gray
rgba(#fff, 0.5)          // semi-transparent white
mix(red, blue)           // purple

// Math functions
percentage(0.5)          // 50%
round(3.6)               // 4
min(10px, 20px)          // 10px

// String functions
to-upper-case("abc")     // "ABC"
str-length("hello")      // 5

// List/Map functions
length(1 2 3)            // 3
map-get(("a": 1), "a")   // 1

// Modern Sass uses modules: @use 'sass:math'; math.div(10, 2)

Practical Function Examples

Functions excel at encapsulating reusable calculations: unit conversions (px/em/rem), z-index management via a map, and design-token lookups. Centralizing these in functions ensures consistency and makes changes (like updating the base font size) propagate everywhere automatically.

sass
// Convert px to em relative to a context
@function em($px, $context: 16px) {
  @return ($px / $context) * 1em;
}

// Generate a z-index from a scale map
$z-indexes: (
  "base": 0,
  "dropdown": 100,
  "modal": 200,
  "toast": 300,
);
@function z($name) {
  @return map-get($z-indexes, $name);
}

// Usage
body { font-size: em(18px); }              // 1.125em
.modal { z-index: z("modal"); }            // 200
.toast { z-index: z("toast"); }            // 300

Recursive Functions

Sass functions support recursion via @if conditionals and self-calls. This enables factorial, Fibonacci, and other computed sequences. However, Sass recursion has no tail-call optimization and is slow — avoid deep recursion in real stylesheets. Prefer loops (@for) for iterative computation.

sass
// Functions can call themselves (recursion)
@function factorial($n) {
  @if $n <= 1 {
    @return 1;
  } @else {
    @return $n * factorial($n - 1);
  }
}

.power { width: factorial(5) * 1px; }  // 120px

// Fibonacci
@function fib($n) {
  @if $n < 2 { @return $n; }
  @return fib($n - 1) + fib($n - 2);
}

// Note: deep recursion is slow in Sass - use sparingly
11

Control Directives

@if / @else if / @else

@if/@else if/@else conditionally outputs styles based on comparisons. Sass uses == and != for equality (not ===). Boolean operators are 'and', 'or', 'not' (not &&, ||, !). Useful for theme switching, feature flags, and responsive logic within mixins and functions.

sass
@mixin theme($mode) {
  @if $mode == dark {
    background: #222;
    color: #eee;
  } @else if $mode == light {
    background: #fff;
    color: #333;
  } @else {
    background: gray;
    color: black;
  }
}

.dark { @include theme(dark); }
.light { @include theme(light); }
.auto { @include theme(auto); }

// Comparison operators: ==, !=, <, >, <=, >=, and, or, not

@for Loop

@for iterates a variable from a start to an end value. 'through' is inclusive (includes the end); 'to' is exclusive (excludes the end). Common for generating grid columns, staggered animations, and numbered utility classes. Use #{} interpolation to insert the counter into selectors.

sass
// @for $var from <start> through <end>  (inclusive)
@for $i from 1 through 3 {
  .col-#{$i} { width: 100% / $i; }
}
// .col-1 { width: 100%; }  .col-2 { width: 50%; }  .col-3 { width: 33.333%; }

// 'to' is EXCLUSIVE (excludes the end)
@for $i from 1 to 3 {
  .item-#{$i} { order: $i; }
}
// .item-1, .item-2  (no .item-3)

// Use 'through' for inclusive, 'to' for exclusive ranges

@each Loop (Lists)

@each iterates over a list, binding each item to a variable. It's ideal for generating themed variants, icon classes, and utility classes from a config list. Lists of lists can be unpacked into multiple variables per iteration for paired data like (name, color) tuples.

sass
// Iterate over a list
$icons: user, home, settings, search;

@each $name in $icons {
  .icon-#{$name} {
    background-image: url("/icons/#{$name}.svg");
  }
}
// .icon-user, .icon-home, .icon-settings, .icon-search

// Multiple values (unpacking)
@each $name, $color in (success green), (error red) {
  .alert-#{$name} { color: $color; }
}

@each with Maps

@each over a map binds both the key and value each iteration. This is the standard way to generate responsive utilities, theme variants, and breakpoint-aware classes from a configuration map. Map iteration order follows insertion order in modern Sass.

sass
$breakpoints: (
  "sm": 576px,
  "md": 768px,
  "lg": 1024px,
  "xl": 1280px,
);

// Iterate key-value pairs of a map
@each $name, $size in $breakpoints {
  .container-#{$name} {
    max-width: $size;
    margin: 0 auto;
  }
}
// .container-sm, .container-md, .container-lg, .container-xl

// Also destructuring lists of maps for richer config
$buttons: (("primary", blue), ("danger", red));

@while Loop

@while loops until its condition becomes false; you must update the counter manually. Use it for non-linear progressions (e.g. doubling each step) where @for doesn't fit. Always ensure the loop terminates — forgetting to increment causes an infinite loop that hangs compilation.

sass
// @while repeats while a condition is true
$i: 1;
@while $i <= 5 {
  .mt-#{$i} { margin-top: $i * 4px; }
  $i: $i + 1;
}
// .mt-1 { margin-top: 4px; } ... .mt-5 { margin-top: 20px; }

// Unlike @for, you control the increment
// WARNING: always increment/decrement to avoid infinite loops!
$i: 6;
@while $i > 0 {
  .delay-#{$i} { transition-delay: $i * 100ms; }
  $i: $i - 1;
}
12

Lists & Maps

Lists

Lists are Sass's array type, separated by spaces or commas. Indexes are 1-based (unlike most languages) — nth($list, 1) returns the first item. Negative indexes count from the end. A single value is treated as a one-element list. Lists can be nested.

sass
// Lists are ordered collections (like arrays)
// Separated by spaces or commas
$sizes: 10px 20px 30px;          // space-separated
$colors: red, green, blue;       // comma-separated
$mixed: (1px 2px) (3px 4px);     // nested lists

// Single value is also a list of length 1
$one: 5px;

// Indexes are 1-based!
$list: a b c;
nth($list, 1);   // a  (NOT 0-indexed)
nth($list, -1);  // c  (negative = from end)
length($list);   // 3

List Functions

List functions: length(), nth(), index() for access; append(), join(), insert-nth() return new lists (lists are immutable). zip() pairs elements from multiple lists. separator() returns the list's separator. Use @each to iterate. The modern API lives in the sass:list module.

sass
$list: a b c;

// Access
length($list);          // 3
nth($list, 2);          // b
index($list, b);        // 2 (position of b, or null)

// Modify (return NEW list - lists are immutable)
append($list, d);       // a b c d
join(a b, c d);         // a b c d
join((a b), (c d), comma); // a, b, c, d

// Insert
insert-nth($list, 2, x);   // a x b c

// Misc
zip(1 2, a b);          // (1 a) (2 b) - pairs elements

Maps

Maps are key-value collections (associative arrays). Keys must be unique; values can be any Sass type including nested maps. Use map-get() to read values, map-has-key() to check existence. Maps are the standard way to model design tokens, breakpoints, and theme palettes.

sass
// Maps are key-value pairs (like objects/dictionaries)
$colors: (
  "primary": #3498db,
  "secondary": #2ecc71,
  "danger": #e74c3c,
  "warning": #f39c12,
);

// Access values
map-get($colors, "primary");      // #3498db

// Check existence
map-has-key($colors, "success");  // false

// Keys must be unique; values can be any type (even nested maps)
$breakpoints: (
  "mobile": (max: 767px),
  "desktop": (min: 768px),
);

Map Functions

Map functions: map-get() reads a value, map-keys()/map-values() return lists, map-merge() combines maps (great for extending themes), map-remove() deletes keys. Maps are immutable — functions return new maps. The modern API is namespaced under the sass:map module.

sass
$colors: ("a": 1, "b": 2);

// Access
map-get($colors, "a");          // 1
map-has-key($colors, "c");      // false

// Keys & values as lists
map-keys($colors);              // ("a", "b")
map-values($colors);            // (1, 2)

// Merge (combines two maps; second wins on conflicts)
$base: ("x": 1, "y": 2);
$override: ("y": 99, "z": 3);
map-merge($base, $override);    // ("x": 1, "y": 99, "z": 3)

// Remove keys
map-remove($colors, "a");       // ("b": 2)

Iterating & Destructuring

@each destructures maps (key, value) and lists of lists, binding multiple variables per iteration. This turns configuration data into generated CSS — a powerful pattern for design systems. Combined with maps, you can drive entire utility-class frameworks from a single config map.

sass
$grid: (
  "sm": 576px,
  "md": 768px,
  "lg": 1024px,
);

// @each destructures map into key+value
@each $name, $size in $grid {
  @media (min-width: $size) {
    .container-#{$name} { max-width: $size; }
  }
}

// Nested destructuring with lists of lists
$config: (("primary", blue, white), ("danger", red, white));
@each $name, $bg, $fg in $config {
  .btn-#{$name} { background: $bg; color: $fg; }
}
13

Color Functions

lighten / darken

lighten() and darken() shift a color's lightness by a percentage. They're deprecated in favor of color.adjust() from sass:color, which is explicit about which channel changes. Pure lighten/darken can produce surprising results on already-extreme colors — prefer HSL-based adjustments.

sass
$base: #3498db;

.light { background: lighten($base, 20%); }   // lighter blue
.dark  { background: darken($base, 20%); }    // darker blue

// Note: lighten/darken are deprecated in modern Sass
// Prefer sass:color adjust() for clarity:
@use 'sass:color';
.adjusted {
  // same effect, explicit
  background: color.adjust($base, $lightness: 20%);
  border-color: color.adjust($base, $lightness: -20%);
}

rgba / rgb / Transparency

rgba($color, $alpha) adds transparency to any color — the most common color function. rgba() also accepts 4 numeric arguments. The modern sass:color module offers color.adjust($alpha: ...) for explicit alpha control. CSS Color 4 space syntax (rgb(r g b / a%)) is also valid in modern Sass.

sass
$brand: #3498db;

// Add alpha to any color
.semi { background: rgba($brand, 0.5); }    // #3498db at 50%
.fade { background: rgba($brand, 0.1); }    // very transparent

// rgba() also accepts 4 numeric args
.box { background: rgba(52, 152, 219, 0.8); }

// Modern alternative: color.adjust for alpha
@use 'sass:color';
.glass { background: color.adjust($brand, $alpha: -0.5); }

// rgb() and rgba() with space syntax (CSS Color 4)
.modern { background: rgb(52 152 219 / 80%); }

mix / Blend Colors

mix($color1, $color2, $weight) blends two colors — $weight is the percentage of $color1 (default 50%). Use mix($color, white) for tints and mix($color, black) for shades, which often look more natural than lighten()/darken(). color.mix is the modern namespaced equivalent.

sass
// mix() blends two colors by a weight (default 50%)
$mix: mix(red, blue);          // purple (50/50)
$mix30: mix(red, blue, 30%);   // 30% red, 70% blue (bluer)

// Useful for generating tints and shades
$base: #3498db;
.tint  { background: mix($base, white, 20%); }  // 20% blue, 80% white
.shade { background: mix($base, black, 20%); }  // 20% blue, 80% black

// sass:color module
@use 'sass:color';
.blended { background: color.mix(red, blue, 50%); }

adjust-color / scale-color / change-color

Three precise color tools: color.adjust() adds deltas to channels, color.scale() scales channels toward their min/max by a percentage (safer — never overshoots), color.change() sets channels to absolute values. The global adjust-color/scale-color/change-color are deprecated aliases.

sass
@use 'sass:color';
$base: #3498db;

// adjust: ADDS to channels (absolute change)
.a { background: color.adjust($base, $red: 30, $alpha: -0.3); }

// scale: SCALES channels by % (relative change, stays in range)
.s { background: color.scale($base, $lightness: 50%); }

// change: SETS channels to exact values (replaces)
.c { background: color.change($base, $lightness: 50%, $hue: 0); }

// adjust moves by a delta, scale shrinks/grows toward min/max,
// change overwrites with a fixed value.

hue / saturation / complement

color.hue/saturation/lightness read HSL channels — useful for conditional logic. color.complement() returns the opposite-hue color (great for accents), color.grayscale() desaturates, color.invert() does RGB inversion. color.hsl() constructs colors from HSL values. These enable programmatic palette generation.

sass
@use 'sass:color';
$c: #3498db;

// Read HSL channels
color.hue($c);           // 204deg
color.saturation($c);    // 70%
color.lightness($c);     // 53%

// Derive new colors
.complement { background: color.complement($c); }     // opposite hue
.grayscale  { background: color.grayscale($c); }       // desaturated
.invert     { background: color.invert($c); }          // RGB inverse

// Build from HSL
.from-hsl { background: color.hsl(204, 70%, 53%); }
14

Math Operations

Basic Arithmetic

Sass supports +, -, *, /, % on numbers and handles units. Since CSS uses / for separation (font: 16px/1.5), plain / as division is deprecated — use math.div() from sass:math, or parentheses. Units must be compatible for + and -. Multiplying two values with units is usually an error.

sass
$base: 16px;

// Sass does math on numbers (with unit handling)
width: 100% - 20px;   // ERROR - incompatible units
width: 100px + 20px;  // 120px
width: 100px * 2;     // 200px
width: 100px / 2;     // 50px (use math.div in modern Sass)
width: (100px / 2);   // 50px (parenthesized = math, not CSS)

// Modern division (the / operator is deprecated for division)
@use 'sass:math';
width: math.div(100px, 2);   // 50px
width: math.div(100, 3);     // 33.333...

Unit Conversion

Sass auto-converts between physical units (px, in, cm, mm, pt, pc) but NOT between px/em/rem/%. You must write conversion functions yourself. The common pattern is a rem($px) function dividing by the base font size. Centralize unit conversion to keep a project's spacing scale consistent.

sass
$base-font: 16px;

// Convert px to rem
@function rem($px) {
  @return math.div($px, $base-font) * 1rem;
}
h1 { font-size: rem(32px); }   // 2rem

// Convert px to em (relative to context)
@function em($px, $context: 16px) {
  @return math.div($px, $context) * 1em;
}

// Sass does NOT auto-convert between px/em/rem/%
// You must compute the ratio yourself.
// 1in = 96px, 1cm = 37.8px, 1pt = 1.33px (these DO auto-convert)

Math Functions

sass:math provides round(), ceil(), floor(), abs(), min(), max(), percentage(), and (newer) trig and root functions plus constants like math.$pi. min()/max() here are Sass functions (different from CSS min()/max() — use the CSS ones unquoted for actual CSS min/max). Always import math explicitly.

sass
@use 'sass:math';

// Rounding
math.round(3.6);     // 4
math.ceil(3.1);      // 4
math.floor(3.9);     // 3

// Absolute & bounds
math.abs(-5);        // 5
math.min(10px, 20px);  // 10px
math.max(10px, 20px);  // 20px

// Percentage conversion
math.percentage(0.5);  // 50%  (math.percentage, or global percentage)

// Trig & constants (Sass 1.65+)
math.sin(math.$pi);    // ~0
math.cos(0);           // 1
math.sqrt(16);         // 4

Modulo & Division

math.div() is the modern division function — the / operator is reserved for CSS list separators (like font: 16px/1.5). math.mod() returns the remainder. math.compatible() checks if two numbers can be added. Always @use 'sass:math' for arithmetic; the global / for division is deprecated and will be removed.

sass
@use 'sass:math';

// Modulo (remainder)
math.unit(10px);     // px
math.compatible(10px, 5px);  // true (can add)
math.is-unitless(5);          // true

// Division MUST use math.div (the / operator is for lists/CSS)
$result: math.div(100, 3);    // 33.333...

// Modulo
$rem: math.mod(10, 3);        // 1

// Generating a grid with modulo
@for $i from 1 through 12 {
  .col-#{$i} { width: math.div($i, 12) * 100%; }
}

Comparison & Logic Operators

Sass uses == and != for equality. Boolean operators are the keywords 'and', 'or', 'not' — not C-style &&/||/!. Use parentheses to clarify precedence. These operators drive @if conditionals inside mixins, functions, and control directives. Truthy values: everything except false and null.

sass
// Comparison: ==, !=, <, >, <=, >=
@if $width > 100px { /* ... */ }
@if $count == 3 { /* ... */ }
@if $mode != "dark" { /* ... */ }

// Boolean logic: and, or, not (NOT &&, ||, !)
$enabled: true;
$mobile: false;

@if $enabled and not $mobile {
  .desktop-only { display: block; }
}

@if $mode == "dark" or $mode == "night" {
  background: black;
}

// Use parentheses to group
@if ($a > 1 and $b < 10) or $c == 5 { /* ... */ }
15

String Functions

quote / unquote

quote() wraps a string in quotes; unquote() removes them. Sass distinguishes quoted strings (used for content, font names) from unquoted strings (used for identifiers, class names). Many functions accept both but the distinction matters for output formatting and selector generation.

sass
// quote() forces quotes around a string
$unquoted: Helvetica;
$quoted: quote($unquoted);   // "Helvetica"

// unquote() removes quotes
$quoted: "Arial";
$plain: unquote($quoted);    // Arial

// Useful when a string needs to be quoted for output
.url { background: url(quote("/img/x.png")); }
// url("/img/x.png")

// Quoted vs unquoted matters for some CSS values
// Class names and property names are usually unquoted

Case Functions

to-upper-case() and to-lower-case() convert string case — useful for generating consistent class names, content values, or normalizing input. Combined with #{} interpolation, they let you build dynamic selectors and content from variables. The modern namespaced versions live in sass:string.

sass
// Change string case
$upper: to-upper-case("hello");   // "HELLO"
$lower: to-lower-case("WORLD");   // "world"

// Practical: generate uppercase class names
$states: success, warning, error;
@each $s in $states {
  .badge-#{to-upper-case($s)} {
    content: "#{to-upper-case($s)}";
  }
}

// .badge-SUCCESS { content: "SUCCESS"; } etc.

str-length / str-slice / str-index

sass:string functions: string.length() returns character count, string.slice($str, $start, $end) returns a substring (1-based, negatives from end), string.index() finds a substring's position (1-based, null if missing). Sass has no built-in split — implement it with a loop using index() and slice().

sass
@use 'sass:string';

$str: "Hello, World";

// Length (character count)
string.length($str);    // 12

// Slice (substring) - 1-based, supports negatives
string.slice($str, 1, 5);   // "Hello"
string.slice($str, 8);      // "World"
string.slice($str, -5);     // "World" (from end)

// Index of substring (1-based, or null if not found)
string.index($str, "World");  // 8
string.index($str, "xyz");    // null

// Split is not built-in - use a custom function with index+slice

str-insert / str-replace

string.insert($string, $insert, $position) inserts a substring at a 1-based position (use a large number to append). string.replace($string, $to-replace, $replace-with, $limit?) swaps substrings (all by default, or limited count). These are essential for programmatic class-name and content generation.

sass
@use 'sass:string';

// str-insert: insert at position (1-based)
string.insert("Hello", "!", 6);    // "Hello!"
string.insert("World", "Hello ", 1);  // "Hello World"

// Modern Sass (1.55+) has string.replace
string.replace("a-b-c", "-", "_");        // "a_b_c" (replace all)
string.replace("a-b-c", "-", "_", 1);     // "a_b-c" (limit 1)

// Build dynamic strings
$prefix: "icon";
$name: string.insert($prefix, "-user", 100);  // "icon-user"

String Interpolation Patterns

#{} interpolation converts any Sass value (numbers, lists, colors, booleans) into its string representation and embeds it. Use it to build dynamic selectors, property names, content values, and URLs. Interpolation is the bridge between Sass values and the CSS text output where plain variables aren't allowed.

sass
$prefix: "btn";
$modifier: "primary";

// #{} converts any value to a string and embeds it
.#{$prefix}-#{$modifier} { /* ... */ }   // .btn-primary

// Interpolating numbers, lists, booleans
$count: 3;
.item-#{$count} { order: $count; }       // .item-3 { order: 3; }

// Interpolating in @include and @media
@mixin respond-to($bp) {
  @media (max-width: $bp) { @content; }
}

// Interpolating function calls
.content::after {
  content: "#{to-upper-case('hello')}";
}
16

@at-root

Basic @at-root

@at-root exits the current nesting context, emitting the nested rule at the top level of the stylesheet. Without @at-root, .child inside .parent compiles to .parent .child (descendant). @at-root makes it just .child. This keeps related rules grouped in source while outputting flat selectors.

sass
.parent {
  color: blue;

  // @at-root jumps out of the nesting
  @at-root .child {
    color: red;
  }
}

// Compiles to (note: .child is NOT nested under .parent):
// .parent { color: blue; }
// .child { color: red; }   <- top-level, not .parent .child

// Useful for defining sibling/related rules inside a block
// without creating a descendant selector.

@at-root with Selectors

Combine @at-root with & to selectively keep parts of the parent selector. @at-root .wrapper & emits .wrapper .component (prepending a context) instead of .component .wrapper. This is the clean way to write theme/context overrides while keeping them co-located with the component code.

sass
.component {
  width: 100px;

  // Combine @at-root with & to keep part of the parent
  @at-root .wrapper & {
    background: yellow;
  }
  // -> .wrapper .component { background: yellow; }

  // Useful for context overrides without deep nesting
  @at-root .dark-theme & {
    color: white;
  }
  // -> .dark-theme .component { color: white; }
}

@at-root for BEM

@at-root helps generate BEM modifier classes (.block--active) as siblings rather than descendants. Without @at-root, nesting .block--active inside .block would create .block .block--active (wrong — that means an active block inside a block). @at-root emits the modifier at the top level, which is the correct BEM output.

sass
.block {
  // State classes as siblings, not descendants
  @at-root {
    .block--active { border-color: blue; }
    .block--disabled { opacity: 0.5; }
  }

  // Or inline form
  @at-root .block--hidden { display: none; }
}

// Compiles to top-level siblings:
// .block { ... }
// .block--active { border-color: blue; }
// .block--disabled { opacity: 0.5; }
// .block--hidden { display: none; }

// Avoids accidentally generating .block .block--active

@at-root with Media Queries

@at-root (with: media) lifts rules out of selector nesting but keeps them inside a @media block. @at-root (without: media) does the opposite. The with/without clauses control which contexts (rule, media, supports) are preserved. This is advanced but essential for building robust responsive mixin libraries.

sass
.component {
  width: 100%;

  // @at-root lifts media queries to the top level
  @at-root (with: media) {
    @media (min-width: 768px) {
      & { width: 50%; }
    }
  }
  // -> @media (min-width: 768px) { .component { width: 50%; } }
}

// Default @at-root (without: rule) lifts OUT of everything
// @at-root (with: media) keeps the @media context but exits rules
// Useful in complex nested mixins to control scope precisely

@at-root without (with)

The (without: ...) and (with: ...) clauses control which nesting contexts @at-root escapes from. Common contexts: 'rule' (selectors), 'media' (@media), 'supports' (@supports), and 'all'. (without: rule) keeps @media but exits selectors. This precise scope control enables advanced mixin and library patterns.

sass
.parent {
  @media (min-width: 768px) {
    .child {
      width: 50%;

      // Exit only the rule, keep the @media
      @at-root (without: rule) {
        .sibling { width: 25%; }
      }
      // -> @media (min-width: 768px) { .sibling { width: 25%; } }

      // Exit everything (default)
      @at-root .top { color: red; }
      // -> .top { color: red; }  (no @media, no parent)
    }
  }
}
17

@content

@content Basics

@content inside a @mixin is a placeholder for a block of styles passed via @include { ... }. The included block replaces @content at the call site. This lets mixins wrap custom styles — essential for media queries, keyframes, and any 'wrapper' pattern where the inner content varies per use.

sass
// @content marks where the included block is injected
@mixin highlighted {
  background: yellow;
  font-weight: bold;

  @content;  // the block passed to @include goes here
}

.title {
  @include highlighted {
    color: red;       // injected where @content was
    font-size: 20px;
  }
}

// Compiles to:
// .title { background: yellow; font-weight: bold; color: red; font-size: 20px; }

@content with Arguments

@content ($args) passes values from the mixin to the included block, which receives them with 'using ($vars)'. This lets mixins provide context (like the current breakpoint name) to the caller's block. Powerful for building iteration/wrapper mixins that need to expose loop state to the consumer.

sass
// Pass values back to the included block (Sass 3.3+)
@mixin each-breakpoint {
  $breakpoints: (sm: 576px, md: 768px, lg: 1024px);

  @each $name, $size in $breakpoints {
    @media (min-width: $size) {
      @content ($name, $size);
    }
  }
}

// Receive via #{...} - actually use the block like a function
@include each-breakpoint using ($name, $size) {
  .container-#{$name} { max-width: $size; }
}

@content for Media Queries

The most common @content use case: a respond-to mixin that wraps styles in the right @media query. Callers pass a block of responsive styles; the mixin places it inside the correct media query. This makes responsive code readable and centralizes breakpoint definitions in one place.

sass
// Classic responsive mixin pattern
@mixin respond-to($breakpoint) {
  @if $breakpoint == phone {
    @media (max-width: 600px) { @content; }
  } @else if $breakpoint == tablet {
    @media (min-width: 601px) and (max-width: 900px) { @content; }
  } @else if $breakpoint == desktop {
    @media (min-width: 901px) { @content; }
  }
}

.sidebar {
  width: 300px;

  @include respond-to(phone) {
    width: 100%;
    float: none;
  }

  @include respond-to(desktop) {
    width: 350px;
  }
}

@content Patterns

@content powers many reusable patterns: @supports wrappers, keyframes generators, and modifier-class generators for BEM. Each lets the caller supply the body while the mixin handles the boilerplate (selector, at-rule, namespace). This is how well-structured Sass libraries provide flexible, composable APIs.

sass
// Wrapper for @supports (feature queries)
@mixin supports-flex { @supports (display: flex) { @content; } }

// Keyframes generator
@mixin keyframes($name) {
  @keyframes #{$name} { @content; }
}

@include keyframes(fade-in) {
  from { opacity: 0; }
  to   { opacity: 1; }
}

// Modifier generator
@mixin modifier($name) {
  &--#{$name} { @content; }
}
.button {
  @include modifier(primary) { background: blue; }
  @include modifier(danger) { background: red; }
}

@content Use Cases

@content enables context wrappers (media/supports/keyframes), extension hooks (base styles + custom additions), and conditional output. It's the Sass equivalent of higher-order functions: the mixin is the framework, @content is the callback. Master @content to build flexible, reusable style utilities.

sass
// 1. Context wrappers (@media, @supports, @keyframes)
@mixin mobile { @media (max-width: 767px) { @content; } }

// 2. Reset/normalize hooks
@mixin base-styles {
  box-sizing: border-box;
  margin: 0;
  @content;  // allow components to add more
}

// 3. Debugging/profiling wrappers
@mixin timed {
  $start: systime();
  @content;
  // log elapsed...
}

// 4. Conditional output
@mixin if-dark($is-dark) {
  @if $is-dark { @content; }
}
18

Module System (@use / @forward)

@use vs @import

@use replaces @import with a proper module system: namespacing prevents collisions, modules load once (cached), members prefixed with - or _ are private (not exposed), and 'with' configures defaults explicitly. This fixes all of @import's structural problems. Use @use for all new Sass code.

sass
// @use is the modern module system (replaces @import)
// Key improvements:
// 1. Members are namespaced (no global pollution)
// 2. Modules load ONCE (cached, no duplicate execution)
// 3. Privacy: members starting with - or _ are private
// 4. Explicit configuration with 'with'

// _shapes.scss
$radius: 4px;
@mixin rounded { border-radius: $radius; }
@mixin -internal { /* private */ }   // not exposed

// app.scss
@use 'shapes';          // namespace: shapes
@use 'shapes' as s;     // namespace: s
@use 'shapes' as *;     // no namespace (global)

.card { @include shapes.rounded; }   // via namespace

@forward

@forward re-exports members from another module without making them your own — perfect for barrel/index files that aggregate a folder's contents. @forward supports 'hide' to exclude members and 'as prefix-*' to rename on re-export. This lets you craft a clean public API for a library or folder.

sass
// @forward re-exports members from another module
// Useful for building index/barrel files

// sass/abstracts/_index.scss
@forward 'variables';    // exposes _variables.scss members
@forward 'functions';
@forward 'mixins';

// Now consumers can import the whole folder at once:
// app.scss
@use 'abstracts' as *;   // gets variables + functions + mixins

// @forward can rename or hide members
@forward 'buttons' hide button-styles;
@forward 'forms' as form-*;  // prefix all members with 'form-'

Private Members (- prefix)

Members whose names start with - or _ are private — they're usable within their own file but NOT exposed through @use or @forward. This lets modules hide implementation details and expose only their public API. Use -prefix for internal helpers, keeping your module's interface clean and stable.

sass
// _math-helpers.scss
@function -internal-double($n) {
  @return $n * 2;
}

@function public-times-two($n) {
  @return -internal-double($n);  // OK - same file
}

// Public function usable via @use
@function scale($n, $factor: 2) {
  @return -internal-double($n) * $factor;
}

// app.scss
@use 'math-helpers';
.val { width: math-helpers.scale(10); }      // OK
// .val { width: math-helpers.-internal-double(10); }  // ERROR - private

Configuring Modules (with)

'with ($var: value, ...)' configures a module's !default variables at load time. This is the modern, scoped replacement for @import's global-variable-override pattern. Configuration applies only to the first @use of a module — subsequent @use statements reuse the configured module. This is how libraries are themed.

sass
// _theme.scss
$primary: blue !default;
$radius: 4px !default;

@mixin button {
  background: $primary;
  border-radius: $radius;
}

// app.scss - configure on @use
@use 'theme' with (
  $primary: #ff6600,
  $radius: 8px,
);

.btn { @include theme.button; }
// background: #ff6600; border-radius: 8px;

// 'with' only works for variables marked !default in the module.
// Configuration must happen at the FIRST @use of that module.

Module Best Practices

Module best practices: use @use (not @import), prefer short explicit namespaces, aggregate folders with @forward in _index.scss, mark internal helpers private with -, configure libraries via 'with' on !default variables, and use the built-in sass:* modules for standard functions. This keeps code modular and maintainable.

sass
// 1. Always use @use, never @import (deprecated)
// 2. Use explicit namespaces for clarity
@use 'sass:math';      // built-in module namespace: math
@use 'variables' as v; // short custom namespace

// 3. Use @forward in _index.scss barrel files
// components/_index.scss
@forward 'buttons';
@forward 'cards';

// 4. Keep members private with - prefix
@function -helper() { @return ...; }

// 5. Configure libraries with 'with', set !default on library vars
$brand: blue !default;

// 6. Use built-in modules: sass:math, sass:color, sass:string,
//    sass:list, sass:map, sass:meta, sass:selector, sass:css
19

Debugging (@debug / @warn / @error)

@debug

@debug prints a value to the terminal during compilation, with the source file and line number. It does not stop the build. Use @debug to inspect variables, trace execution flow, and verify computations while developing. Remove or comment out @debug statements before shipping. Output goes to stderr, not the CSS.

sass
// @debug prints a message to the console during compilation
$base: 16px;
$size: 24px;

@debug "Calculating size...";
@debug "base is #{$base}, size is #{$size}";
@debug "ratio: " #{math.div($size, $base)};

// Console output during compile:
// _file.scss:5 DEBUG: Calculating size...
// _file.scss:6 DEBUG: base is 16px, size is 24px

// @debug does NOT stop compilation - just logs
// Use it to trace values while developing

@warn

@warn prints a non-fatal warning to the console during compilation — the build continues. Use it for deprecation notices, fallback behavior, and potential misuse alerts in libraries. @warn includes a stack trace by default so users can locate the warning source. Like @debug, it doesn't appear in the CSS output.

sass
// @warn prints a warning to the console (does NOT stop compilation)
@mixin deprecated-mixin {
  @warn "deprecated-mixin is deprecated; use new-mixin instead.";
  /* ... styles ... */
}

// Useful for deprecation notices, potential issues
@warn "Variable $primary is not set, falling back to default.";

// With stack trace for locating the call site
@warn "Invalid value: #{$value}" {
  // optional stack trace via meta.module-functions
}

// @warn is non-fatal - compilation continues.

@error

@error throws a fatal error, halting compilation immediately. Use it to validate inputs to functions and mixins — catching misuse early with a clear message beats emitting broken CSS. @error includes a stack trace. Reserve @error for genuinely invalid states; use @warn for recoverable issues and @debug for inspection.

sass
// @error stops compilation with an error message
@function divide($a, $b) {
  @if $b == 0 {
    @error "Cannot divide by zero: divide(#{$a}, #{$b})";
  }
  @return math.div($a, $b);
}

// In a mixin validating arguments
@mixin set-font-size($size) {
  @if not $size {
    @error "set-font-size requires a non-null $size, got #{$size}";
  }
  font-size: $size;
}

// @error halts the build - use for invalid inputs
// that would produce broken CSS.

meta Module Functions

sass:meta provides introspection: type-of() returns a value's type, inspect() produces a readable string of any value (great for debugging), and the *-exists functions check for variables/mixins/functions by name. meta.get-function() returns a callable reference, enabling higher-order patterns. Use these for defensive library code.

sass
@use 'sass:meta';

// Inspect values
meta.type-of(10px);          // number
meta.type-of("hi");          // string
meta.type-of((a: 1));        // map
meta.inspect((a: 1, b: 2));  // (a: 1, b: 2) - readable representation

// Variable existence (within current scope)
meta.variable-exists("base");    // true if $base exists
meta.global-variable-exists("primary");

// Mixin/function existence
meta.mixin-exists("my-mixin");
meta.function-exists("my-func");

// Feature queries
meta.feature-exists("at-error");  // true

// Get a function reference (for higher-order use)
$fn: meta.get-function("lighten");

Inspection Functions

type-of() (global or meta.type-of) identifies a value's data type — essential for defensive function/mixin logic. inspect() renders any Sass value (including maps and nested lists) as a readable string, perfect for @debug logging. math.unit()/is-unitless() inspect numbers. Together these tools make Sass internals observable during development.

sass
@use 'sass:meta';
@use 'sass:math';

// type-of: get the type of any value
$value: 16px;
type-of($value);          // number
type-of("hello");         // string
type-of(#fff);            // color
type-of(true);            // bool
type-of(null);            // null
type-of(1 2 3);           // list
type-of((a: 1));          // map

// inspect: readable debug output of complex values
$map: (primary: blue, secondary: green);
debug: inspect($map);  // (primary: blue, secondary: green)

// unit / unitless for numbers
math.unit(16px);         // px
math.is-unitless(16);    // true

// Combine with @debug for tracing
@debug "got: #{inspect($map)}";
20

Sass vs SCSS Syntax

SCSS Syntax (.scss)

SCSS (.scss) is the most popular Sass syntax. It's a strict superset of CSS — any valid CSS file is valid SCSS, so you can rename .css to .scss and start adding Sass features incrementally. SCSS uses braces and semicolons like CSS. Choose SCSS for new projects and team adoption.

sass
// SCSS - Sassy CSS
// Uses braces {} and semicolons ;
// Superset of CSS - all CSS is valid SCSS

$primary: #3498db;

.button {
  background: $primary;

  &:hover {
    background: darken($primary, 10%);
  }

  .icon {
    width: 16px;
    height: 16px;
  }
}

// Pros: familiar to CSS devs, easy migration, widely adopted

Sass Indented Syntax (.sass)

The Sass indented syntax (.sass) drops braces and semicolons, relying solely on indentation (like Python or Stylus). It's terser but you can't paste raw CSS into a .sass file unchanged, and indentation errors cause bugs. Less common than SCSS but preferred by some for its clean look. Both compile to identical CSS.

sass
// Sass - indented syntax
// NO braces, NO semicolons - uses indentation only
// Removes CSS punctuation for a cleaner look

$primary: #3498db

.button
  background: $primary

  &:hover
    background: darken($primary, 10%)

  .icon
    width: 16px
    height: 16px

// Pros: less typing, cleaner visually
// Cons: can't paste CSS directly, less popular, error-prone indentation

Converting Between Syntaxes

sass-convert (bundled with Sass) converts between .scss and .sass. In Sass syntax, @mixin becomes = and @include becomes + for brevity. Beyond syntax, both are identical — same features, same output CSS. Most teams standardize on SCSS for its CSS compatibility and broader tooling support.

sass
// Convert SCSS to Sass (and vice versa) with the sass CLI:
// $ sass-convert style.scss style.sass
// $ sass-convert style.sass style.scss

// Bulk convert a folder:
// $ sass-convert --recursive --from scss --to sass scss/ sass/
// $ sass-convert --recursive --from sass --to scss sass/ scss/

// Key syntax differences:
// 1. SCSS uses { } and ;  |  Sass uses indentation
// 2. SCSS: @include mixin()  |  Sass: +mixin()
// 3. SCSS: @mixin name {}    |  Sass: =name
// 4. SCSS: @import "x"       |  Sass: same
// 5. Multi-line: SCSS needs ; | Sass just newlines

When to Use Each

SCSS is the safer default — it's the industry standard, every CSS file is valid SCSS, and most documentation/examples use it. The indented .sass syntax appeals to developers who dislike punctuation but is less widely supported by tooling and harder to onboard new team members into. Pick one per project and stay consistent.

sass
// Use SCSS (.scss) when:
// - Starting a new project (most common choice)
// - Migrating an existing CSS codebase (CSS is valid SCSS)
// - Working on a team (most developers know SCSS)
// - Using CSS frameworks/libraries (they're usually SCSS)

// Use Sass (.sass) when:
// - You prefer minimal punctuation / Python-like syntax
// - Writing from scratch with no CSS to migrate
// - Personal preference for cleaner-looking source

// Both produce identical CSS. The choice is stylistic.
// IMPORTANT: don't mix .scss and .sass in the same project.

Common Pitfalls

Modern Sass deprecates several legacy features: use math.div() instead of / for division, @use/@forward instead of @import, and the sass:color/sass:math module functions instead of global lighten()/darken()/mix() etc. The sass-migrator tool automates these migrations. Watch deprecation warnings during compilation to keep code future-proof.

sass
// 1. Division: / is deprecated for math, use math.div()
@use 'sass:math';
width: math.div(100px, 2);   // CORRECT
// width: 100px / 2;          // DEPRECATED (may be CSS list separator)

// 2. @import is deprecated - use @use/@forward
@use 'variables';             // CORRECT
// @import 'variables';        // DEPRECATED

// 3. lighten()/darken() deprecated - use color.adjust
@use 'sass:color';
color.adjust($c, $lightness: 10%);   // CORRECT
// lighten($c, 10%);                  // DEPRECATED

// 4. Global color functions deprecated - use sass:color module
color.mix(red, blue);          // CORRECT (via @use 'sass:color')
// mix(red, blue);              // DEPRECATED global function

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.