Concept
Sass (Syntactically Awesome StyleSheets), almost always used via its SCSS syntax (CSS-superset syntax, as opposed to Sass's original indentation-based syntax), is a CSS preprocessor, it compiles to plain CSS at build time, adding features the CSS spec didn't originally have: variables, nesting, mixins, functions, loops, and module imports with actual scoping. Many of these have since been added natively to CSS itself (custom properties, native nesting, @media range syntax), the honest, current evaluation of Sass is "what does it still do that native CSS genuinely can't," not a blanket "you need a preprocessor" assumption from 2015.
Variables, compile-time, unlike custom properties
$primary-color: #4f46e5;
$spacing-unit: 8px;
.button {
background: $primary-color;
padding: $spacing-unit * 2; // real arithmetic, no calc() needed
}Sass variables are resolved entirely at build time, by the time the compiled CSS reaches the browser, $primary-color is gone, replaced everywhere with the literal #4f46e5. This is the fundamental contrast with native CSS custom properties (covered in their own topic): no runtime changeability, no cascade/inheritance behavior, no JS read/write access, but genuinely simpler arithmetic ($spacing-unit * 2 works directly, no calc() wrapper needed) since it's just compile-time math, not a runtime CSS feature.
Nesting, now natively available in CSS, but Sass had it first
.card {
padding: 1rem;
&__title { // & = parent selector reference, compiles to .card__title
font-size: 1.25rem;
}
&:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dark & { // & can appear anywhere in the nested selector, not just prefixed
background: #1f2937;
}
}Modern CSS now supports native nesting with very similar syntax and semantics (covered in the Modern CSS topic), this is one of the clearest examples of a Sass feature the language itself has since absorbed. For a codebase not otherwise using Sass for its other features, native nesting alone is no longer sufficient reason to add the preprocessor.
Mixins, reusable, parameterized style blocks
@mixin flex-center($direction: row) {
display: flex;
align-items: center;
justify-content: center;
flex-direction: $direction;
}
.card { @include flex-center(column); }Mixins let you define reusable, parameterized chunks of CSS, something native CSS still has no direct equivalent for. Custom properties + calc() can approximate simple cases, but a mixin with conditional logic, loops, or multiple output properties genuinely has no native CSS equivalent yet.
Functions, computed values at build time
@function rem($px) {
@return calc($px / 16px) * 1rem;
}
.title { font-size: rem(24px); } // → font-size: 1.5rem;Sass functions compute a value and return it, usable anywhere a value is expected, genuine reusable computation logic, evaluated once at build time.
Loops and control flow, generating repetitive CSS programmatically
@each $name, $color in (primary: #4f46e5, danger: #dc2626, success: #10b981) {
.badge--#{$name} {
background: $color;
}
}@for $i from 1 through 12 {
.col-#{$i} { width: percentage($i / 12); }
}This is genuinely something native CSS has no equivalent for at all, generating a whole family of related rules programmatically (a 12-column grid system, a full color/size variant matrix) from a compact loop, rather than writing every variant out by hand. This remains one of Sass's strongest, still-uniquely-valuable features.
Partials and @use, real module scoping
// _variables.scss (leading underscore = "partial," not compiled to its own CSS file)
$primary-color: #4f46e5;
// main.scss
@use "variables" as vars;
.button { background: vars.$primary-color; }The modern @use module system (replacing the older, globally-scoped @import) gives genuine namespaced scoping to Sass variables/mixins/functions across files, avoiding the global-namespace collision problem that plain CSS @import and the older Sass @import both had. This is a real, still-relevant capability difference from plain CSS.
Common Mistakes
1. Reaching for Sass purely for variables or nesting in a new project
Both are now natively available in CSS (custom properties, native nesting) without a build step. Adding an entire preprocessor toolchain to a new project solely for these two features is no longer the clear win it was years ago, evaluate whether the project genuinely needs mixins, loops, or the module system before adding the dependency.
2. Using the older, globally-scoped @import instead of @use
/* Old, globally scoped, order-dependent, can cause duplicate output if imported multiple times */
@import "variables";
@import "mixins";/* Modern, namespaced, avoids collisions and duplicate compilation */
@use "variables" as vars;
@use "mixins";Sass's own documentation has deprecated @import in favor of @use/@forward specifically because of the global-namespace and duplicate-compilation problems the old system had, new Sass code should use the module system.
3. Deep nesting that recreates the exact specificity problem BEM/ITCSS warn against
// Compiles to .page .sidebar .widget .title, high specificity, hard to override later
.page {
.sidebar {
.widget {
.title { }
}
}
}Sass's nesting convenience makes it easy to accidentally recreate deep-descendant-selector specificity problems, the same discipline covered in the CSS Architecture topic (flat BEM-style naming, limited nesting depth) still applies; nesting syntax convenience doesn't exempt you from specificity consequences.
4. Overusing @extend and being surprised by the generated output
.button { padding: 8px 16px; }
.button-primary { @extend .button; background: blue; }@extend merges the extending selector into the original rule's selector list at the point the original rule was defined, which can produce larger, harder-to-predict combined selectors than the equivalent @include (mixin) approach, especially across multiple extends. Modern Sass guidance generally favors mixins over @extend for predictability.
5. Not leveraging Sass's actual unique strengths, using it just as "CSS with a different file extension"
If a codebase uses .scss files but never touches mixins, functions, loops, or the module system, the meaningful question is whether the build-step overhead is earning its keep versus plain CSS with custom properties and native nesting, Sass's genuine remaining value is concentrated in the programmatic/computational features, not the parts CSS has since absorbed.
Best Practices
- Evaluate against current native CSS capabilities, not a 2015-era assumption that any nontrivial project needs a preprocessor, variables and nesting alone no longer justify it.
- Reach for Sass specifically for mixins, loops/control flow, and true build-time computation, its remaining genuinely unique strengths.
- Use
@use/@forward, not the deprecated@import, for any new Sass module organization. - Keep nesting shallow and BEM-flat-conscious, Sass's nesting convenience doesn't exempt a codebase from specificity discipline.
- Prefer mixins over
@extendfor predictable, easy-to-reason-about compiled output. - Combine Sass variables (build-time computation, color functions like /) with native CSS custom properties (runtime theming) where a project genuinely benefits from both, they're not mutually exclusive.
Further Resources
- Sass, official docs
- Sass,
@useand modules - Sass, the deprecation of @import
- CSS-Tricks, Sass basics
- MDN, CSS nesting (native), for comparing to what Sass nesting already offered.
