Skip to main content

CSS preprocessors: SASS, SCSS, and LESS

CSS preprocessors compile an extended CSS syntax with variables, nesting, mixins, and functions into plain CSS that browsers can read.

Theory

TL;DR

  • Preprocessors work like a word processor with mail merge: write structured templates with reusable parts, compile to plain CSS for the browser.
  • SCSS is a strict superset of CSS (any valid CSS is valid SCSS); original SASS uses indentation instead of braces; LESS uses @ for variables and compiles via JS.
  • Main syntax split: SCSS and LESS use braces and semicolons; SASS uses indentation only.
  • Decision rule: SCSS for teams (easiest CSS migration); LESS for dynamic JS-driven theming; skip both if CSS custom properties cover your needs.
  • Bootstrap 5.3, Angular 18, and Bulma all ship with SCSS.

Quick example

scss
// input.scss $blue: #007bff; .nav { background: $blue; .item { padding: 1rem; &:hover { background: lighten($blue, 10%); // computes to #4dabf7 } } }
css
/* Compiled output */ .nav { background: #007bff; } .nav .item { padding: 1rem; } .nav .item:hover { background: #4dabf7; }

The compiler reads the nested structure, flattens it into standard selectors, and resolves all variable references in one pass.

Key difference

SCSS is 100% CSS-compatible. Paste any existing stylesheet into a .scss file and it compiles without changes. That alone makes migration painless. Original SASS trades braces and semicolons for significant whitespace, which some developers prefer for its minimal look. LESS looks similar to SCSS on the surface but uses @variable syntax, and its JS-based compiler can struggle with complex loops in large codebases. Most React projects I've worked on settled on SCSS once the component count grew past 20, simply because existing CSS snippets drop in without edits.

When to use

  • Team project with existing CSS: SCSS - zero friction migrating files.
  • Dynamic theming via JS (like Ant Design 5): LESS - its @var system integrates naturally with JS config objects.
  • Small project or prototype: skip preprocessors, use CSS custom properties (--color: red;) - supported in all major browsers since 2016.
  • Angular or React/Vite app: any of the three - sass-loader or Vite's built-in Sass support handles compilation.
  • Ruby stack: SASS - deepest ecosystem there.

Comparison table

FeatureSASS (.sass)SCSS (.scss)LESS (.less)
SyntaxIndentation onlyBraces + semicolonsBraces + semicolons
CSS supersetNoYes (100%)Yes (mostly)
Variables$var$var@var
Mixins@mixin@mixin.mixin()
CompilationRuby/NodeRuby/Node/DartJS/Node/browser
Loops@each, @for@each, @forGuards, recursive mixins
When to useSolo Ruby devsTeams, CSS migrationJS projects, dynamic vars

How compilation works

You write .scss, .sass, or .less files. A compiler (Dart Sass for SCSS/SASS since v1.77+, lessc for LESS v4.1+) parses the source into an abstract syntax tree, resolves all variables and mixin calls in a single pass, then emits standard CSS. Tools like sass-loader for Webpack v14+ or Vite's built-in support watch files and recompile on save, outputting to /dist. Browsers never see the source files.

Common mistakes

Nesting more than 3-4 levels deep

scss
// Wrong - produces .a .b .c .d .e { color: red; } a { b { c { d { e { color: red; } } } } }

Deeply nested selectors bloat compiled output and slow down selector matching. Chrome DevTools flags selectors beyond 6 levels. Keep nesting to 3 levels max, or use BEM: .nav__item--active { color: red; }.

Global variable overrides across files

scss
// _theme.scss $color: blue; // _component.scss imports _theme.scss, then: $color: red; // overrides globally

Webpack's partial recompilation can miss dependencies and produce inconsistent output. Use @use 'theme' as *; with Sass modules (v1.23+). It namespaces variables and computes each file only once.

Using @import instead of @use

@import dumps everything into the global namespace and re-executes the file every time it appears. @use loads the file once and namespaces its variables as theme.$primary. Dart Sass deprecated @import in v1.80.

Browser-compiling LESS

The old pattern of loading less.js in the browser produces no source maps and breaks on modern mixins. Always pre-compile with lessc or a bundler.

LESS lazy evaluation in loops

less
// LESS - guard-based loops can skip values unexpectedly .loop(@n) when (@n > 0) { .w-@{n} { width: ~"@{n}%"; } .loop((@n - 5)); } .loop(25);

For predictable loops, SCSS @for is a cleaner option:

scss
@for $i from 1 through 5 { .w-#{$i * 5} { width: $i * 5%; } }

Real-world usage

  • Bootstrap 5.3: SCSS variables and mixins for theming ($primary, @mixin button-variant).
  • Ant Design 5: LESS for dynamic theming with JS-driven variable overrides.
  • Angular 18: built-in SCSS support with :host component nesting.
  • Bulma 1.0: pure SCSS with CSS custom property fallback.
  • React/Vite: sass-loader plus SCSS modules per component.

Follow-up questions

Q: What is the difference between @mixin and @extend in SCSS?
A: @mixin copies the declared styles into every selector that includes it, which can bloat output. @extend makes multiple selectors share one rule block, which is leaner but order-sensitive. Use @extend for utility classes like .btn; use @mixin when you need to pass arguments.

Q: How do you handle vendor prefixes with SCSS?
A: Run Autoprefixer as a PostCSS plugin after compilation. It reads your browserslist config and adds -webkit-, -moz-, etc. automatically. The preprocessor handles logic; Autoprefixer handles prefixes.

Q: What is the difference between @use and @import in Sass?
A: @import pollutes the global namespace and re-executes the file on each import. @use loads the file once and exposes its members under a namespace (theme.$primary). Introduced in Dart Sass v1.23; @import is deprecated since v1.80.

Q: How does Dart Sass differ from LibSass?
A: Dart Sass is the current official implementation (v1.77+ as of 2024) and tracks the spec. LibSass was a C++ port deprecated in 2021 that lagged on @use and @forward. If you are still on node-sass, migrate to the sass package.

Q (senior): In a monorepo with 100+ SCSS partials, how do you prevent rebuild cascades?
A: Use @use instead of @import so each partial computes once. Combine with Vite's ?inline imports for component-level HMR. Profile slow builds with sass --trace and avoid glob imports - they break dependency tracking.

Examples

Themeable navbar with breakpoints

A React app needs a navbar that adapts to screen size and pulls colors from a shared theme file.

scss
// styles/navbar.scss $primary: #0d6efd; $breakpoint-md: 768px; @mixin flex-center { display: flex; align-items: center; } .navbar { @include flex-center; padding: 1rem; background: $primary; @media (min-width: $breakpoint-md) { padding: 1.5rem; } .logo { font-size: 1.5rem; } &__link { color: white; text-decoration: none; &:hover { color: lighten($primary, 20%); } } }
css
/* Compiled output (simplified) */ .navbar { display: flex; align-items: center; padding: 1rem; background: #0d6efd; } @media (min-width: 768px) { .navbar { padding: 1.5rem; } } .navbar .logo { font-size: 1.5rem; } .navbar__link { color: white; text-decoration: none; } .navbar__link:hover { color: #6ea8fe; }

The mixin handles flex setup, $primary is declared once at the top, and the media query is colocated with the component it affects - not buried in a separate breakpoints file.

Sass modules with @use

This pattern replaces @import in any Dart Sass project (v1.23+) and is the current recommended approach.

scss
// _theme.scss $primary: #0d6efd; $danger: #dc3545; $font-size-base: 1rem;
scss
// button.scss @use 'theme'; .btn { font-size: theme.$font-size-base; background: theme.$primary; &--danger { background: theme.$danger; } }

The theme. prefix makes it clear where each variable originates. No more hunting through 50 partials to find which file last set $primary.

Control flow: SCSS vs LESS

Generating spacing utility classes with a loop is a common task. Here is how both preprocessors handle it.

scss
// SCSS - predictable @for loop @for $i from 1 through 5 { .mt-#{$i} { margin-top: $i * 0.25rem; // 0.25, 0.5, 0.75, 1, 1.25rem } }
less
// LESS - recursive mixin guard (equivalent result) .spacing-loop(@n) when (@n > 0) { .mt-@{n} { margin-top: (@n * 0.25rem); } .spacing-loop((@n - 1)); } .spacing-loop(5);

Both produce the same five classes. The SCSS version reads as a standard loop; the LESS version requires understanding guard-based recursion before you can safely modify it.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?