Frontend

CSS Preprocessors: SASS vs LESS

Mayur Dabhi
Mayur Dabhi
April 17, 2026
14 min read

Someone asks you to change the brand purple, and you find the same hex code in 40 places across six stylesheets, three of them slightly different shades. Or you open a component file and every selector repeats .dashboard .sidebar .nav before getting to the part you care about. That's the point where plain CSS stops scaling, and it's the problem preprocessors were built for: you write a richer syntax, and a compiler turns it into normal CSS for the browser.

The two you'll run into most are SASS (Syntactically Awesome Style Sheets) and LESS (Leaner Style Sheets). They overlap a lot, but they differ in scoping, logic, and tooling. Below I compare them feature by feature (variables, nesting, mixins, functions, file structure), show how to set each one up, and finish with when I'd pick one over the other.

Quick Context

SASS was created by Hampton Catlin in 2006 and is written in Ruby/Dart. LESS was created by Alexis Sellier in 2009 and runs on JavaScript (Node.js). Today, SCSS (a superset of CSS, part of the SASS ecosystem) is the most widely used preprocessor syntax, while LESS lives on in Bootstrap 3 and plenty of older codebases. Bootstrap switched to SASS in version 4.

What Are CSS Preprocessors?

Browsers never see your .scss or .less files. You write in the preprocessor's syntax, a compiler runs at build time, and what ships is a regular CSS file. What you get in exchange for that build step:

.scss / .less Source File Preprocessor Compiler / Build Tool (Dart Sass / Less.js) style.css Browser-Ready CSS CSS Preprocessor Compilation Flow

How CSS preprocessors transform source files into browser-ready CSS

Variables: The Foundation

This is the feature most people adopt a preprocessor for. Instead of copy-pasting your brand color across 50 files, you define it once and reference it everywhere. When the client wants a slightly different shade of blue, you change one line.

SASS Variables

SASS uses the $ prefix for variables. They're scoped to the block they're declared in, and you can make them globally available with the !global flag.

variables.scss
// === Design Tokens ===
$color-primary:    #7c3aed;
$color-secondary:  #4f46e5;
$color-success:    #10b981;
$color-danger:     #ef4444;
$color-warning:    #f59e0b;

// Typography
$font-base:        'Inter', sans-serif;
$font-mono:        'Fira Code', monospace;
$font-size-base:   16px;
$font-size-sm:     14px;
$font-size-lg:     20px;

// Spacing Scale
$spacing-xs:       4px;
$spacing-sm:       8px;
$spacing-md:       16px;
$spacing-lg:       24px;
$spacing-xl:       48px;

// Breakpoints
$breakpoint-sm:    576px;
$breakpoint-md:    768px;
$breakpoint-lg:    1024px;
$breakpoint-xl:    1280px;

// Usage
.btn-primary {
    background-color: $color-primary;
    font-family: $font-base;
    padding: $spacing-sm $spacing-md;

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

.card {
    border-radius: $spacing-sm;
    padding: $spacing-lg;
    font-size: $font-size-base;
}

LESS Variables

LESS uses the @ prefix, which is the same symbol CSS uses for at-rules like @media and @keyframes. It can be confusing when you're scanning a file, but in practice it's a minor difference.

variables.less
// === Design Tokens ===
@color-primary:    #7c3aed;
@color-secondary:  #4f46e5;
@color-success:    #10b981;
@color-danger:     #ef4444;
@color-warning:    #f59e0b;

// Typography
@font-base:        'Inter', sans-serif;
@font-size-base:   16px;
@font-size-lg:     20px;

// Spacing Scale
@spacing-sm:       8px;
@spacing-md:       16px;
@spacing-lg:       24px;

// Usage
.btn-primary {
    background-color: @color-primary;
    font-family: @font-base;
    padding: @spacing-sm @spacing-md;

    &:hover {
        background-color: darken(@color-primary, 10%);
    }
}

.card {
    padding: @spacing-lg;
    font-size: @font-size-base;
}
Variable Scoping Difference

SASS variables are block-scoped and evaluated in order: a variable declared inside a selector only exists in that block, and its value is whatever it was at the line where you use it. LESS variables are also scoped, but they're lazily evaluated. Inside a given scope the last definition wins, even if it comes after the line that uses it. Redefine @color-primary at the bottom of a file and every earlier use picks up the new value, which is a fun one to debug in a big LESS codebase.

Nesting: Mirror Your HTML Structure

Instead of repeating parent selectors, you write child selectors inside their parent, so the stylesheet follows the shape of your markup and related rules stay together. Don't go more than three levels deep, though. Deep nesting produces long, high-specificity selectors that are painful to override later.

Both SASS and LESS support nesting with almost identical syntax. The key difference is how they handle the parent selector reference &:

nesting.scss (SASS/SCSS)
// Navigation component with nesting
.nav {
    display: flex;
    background: $color-primary;
    padding: $spacing-md;

    &__logo {
        font-size: 1.5rem;
        font-weight: 700;
        color: #fff;
    }

    &__links {
        display: flex;
        gap: $spacing-md;
        margin-left: auto;
    }

    &__link {
        color: rgba(255, 255, 255, 0.8);
        text-decoration: none;
        padding: $spacing-xs $spacing-sm;
        border-radius: 6px;
        transition: all 0.3s;

        &:hover {
            color: #fff;
            background: rgba(255, 255, 255, 0.15);
        }

        &--active {
            color: #fff;
            background: rgba(255, 255, 255, 0.2);
            font-weight: 600;
        }
    }

    // Media query nesting (SASS 3.3+)
    @media (max-width: $breakpoint-md) {
        flex-direction: column;

        &__links {
            flex-direction: column;
            margin-left: 0;
        }
    }
}

// Compiles to:
// .nav { display: flex; ... }
// .nav__logo { ... }
// .nav__links { ... }
// .nav__link { ... }
// .nav__link:hover { ... }
// .nav__link--active { ... }
// @media (max-width: 768px) { .nav { ... } ... }
The BEM Pattern and SASS

SASS's & parent selector fits BEM (Block Element Modifier) naming well. Writing &__element and &--modifier inside a block produces the right BEM class names. The downside: you can't grep your codebase for .nav__link anymore, because that string never appears in the source.

Mixins: Reusable Style Blocks

Think of mixins as functions that output declarations. You define a block of styles once, include it wherever you need it, and pass arguments to tweak it. Keep in mind that every include copies the whole block into the output, so a big mixin used in 30 places means 30 copies in your CSS.

SASS Mixins

mixins.scss
// Flexbox centering mixin
@mixin flex-center($direction: row) {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: $direction;
}

// Responsive breakpoint mixin
@mixin respond-to($breakpoint) {
    @if $breakpoint == 'sm' {
        @media (max-width: $breakpoint-sm) { @content; }
    } @else if $breakpoint == 'md' {
        @media (max-width: $breakpoint-md) { @content; }
    } @else if $breakpoint == 'lg' {
        @media (max-width: $breakpoint-lg) { @content; }
    }
}

// Button variant mixin
@mixin button-variant($bg, $text: #fff, $hover-darken: 10%) {
    background-color: $bg;
    color: $text;
    border: 2px solid transparent;
    border-radius: 8px;
    padding: $spacing-sm $spacing-lg;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s;

    &:hover {
        background-color: darken($bg, $hover-darken);
        transform: translateY(-1px);
        box-shadow: 0 4px 12px rgba($bg, 0.4);
    }

    &:active {
        transform: translateY(0);
    }
}

// Truncate text mixin
@mixin truncate($lines: 1) {
    @if $lines == 1 {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    } @else {
        display: -webkit-box;
        -webkit-line-clamp: $lines;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }
}

// Usage
.hero {
    @include flex-center(column);
    height: 100vh;
}

.btn-primary   { @include button-variant($color-primary); }
.btn-danger    { @include button-variant($color-danger); }
.btn-success   { @include button-variant($color-success); }

.article-title {
    @include truncate(2);
    font-size: $font-size-lg;
}

.sidebar {
    width: 300px;

    @include respond-to('md') {
        width: 100%;
    }
}

LESS Mixins

LESS mixins look like class selectors. That makes them easy to write, but when you read .flex-center(); it's not obvious if that's a mixin call or a real class being pulled in. (Any class in LESS can be used as a mixin, which adds to the confusion.)

mixins.less
// Flexbox centering mixin
.flex-center(@direction: row) {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: @direction;
}

// Button variant mixin
.button-variant(@bg; @text: #fff) {
    background-color: @bg;
    color: @text;
    border-radius: 8px;
    padding: @spacing-sm @spacing-lg;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s;

    &:hover {
        background-color: darken(@bg, 10%);
    }
}

// Truncate text mixin
.truncate(@lines: 1) when (@lines = 1) {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

// Usage
.hero {
    .flex-center(column);
    height: 100vh;
}

.btn-primary { .button-variant(@color-primary); }
.btn-danger  { .button-variant(@color-danger); }

.article-title {
    .truncate(1);
    font-size: @font-size-lg;
}

Functions and Operations

Both let you do math and call built-in functions on values. That's handy for type scales, grid widths, and color variations. SASS goes further and lets you write real functions with @return.

functions.scss (SASS)
// Custom SASS functions
@function rem($px, $base: 16) {
    @return #{$px / $base}rem;
}

@function z($layer) {
    $layers: (
        'below':    -1,
        'base':      0,
        'content':  10,
        'overlay':  100,
        'modal':    200,
        'toast':    300,
    );
    @return map-get($layers, $layer);
}

@function color-contrast($background) {
    $luminance: (red($background) * 0.299 + green($background) * 0.587 + blue($background) * 0.114) / 255;
    @if $luminance > 0.5 {
        @return #000;
    } @else {
        @return #fff;
    }
}

// Math operations
$base-size:    16px;
$golden-ratio: 1.618;

.type-scale {
    font-size: $base-size;

    h1 { font-size: $base-size * $golden-ratio * $golden-ratio * $golden-ratio; }
    h2 { font-size: $base-size * $golden-ratio * $golden-ratio; }
    h3 { font-size: $base-size * $golden-ratio; }
}

// Color functions
.theme-button {
    $bg: #7c3aed;
    background-color: $bg;
    color: color-contrast($bg);
    border-color: darken($bg, 15%);

    &:hover {
        background-color: lighten($bg, 5%);
        box-shadow: 0 0 20px rgba($bg, 0.5);
    }
}

// Grid system with math
$columns:  12;
$gutter:   24px;

@for $i from 1 through $columns {
    .col-#{$i} {
        width: percentage($i / $columns);
        padding: 0 $gutter / 2;
    }
}
operations.less (LESS)
// LESS operations and built-in functions
@base-size:    16px;
@gutter:       24px;
@columns:      12;

// Math operations
.container {
    max-width: (@base-size * 75);  // 1200px
    padding: 0 (@gutter / 2);
    margin: 0 auto;
}

// Color manipulation
@primary: #7c3aed;
.theme-colors {
    background: @primary;
    border:     darken(@primary, 15%);
    highlight:  lighten(@primary, 20%);
    soft:       fade(@primary, 20%);
    text:       contrast(@primary);
}

// String interpolation
@prefix: 'col';
.@{prefix}-6 {
    width: percentage(6 / @columns);
}

// Guard mixins (conditional logic)
.text-size(@size) when (@size >= 24) {
    font-size: @size * 1px;
    line-height: 1.3;
}
.text-size(@size) when (@size < 24) {
    font-size: @size * 1px;
    line-height: 1.6;
}
The Division Warning You'll Hit in Dart Sass

Run the SASS example above through a current Dart Sass and you'll get Deprecation Warning: Using / for division outside of calc() is deprecated for lines like $px / $base and $gutter / 2. The fix is @use "sass:math"; at the top of the file and math.div($px, $base) instead. On older codebases this warning can show up hundreds of times per build and bury real errors, so it's worth running sass-migrator division once. LESS has a similar trap: since Less 4, division only happens inside parentheses by default, so @gutter / 2 without them is output as-is.

Partials and Modular Architecture

Splitting styles into small files is where a preprocessor pays off most in a real project. Both SASS and LESS support it. Here's a common way to lay out a larger SCSS codebase:

main.scss Entry Point abstracts/ variables, mixins, functions base/ reset, typography, colors components/ buttons, cards, modals pages/ home, blog, dashboard _variables.scss _mixins.scss _functions.scss _reset.scss _typography.scss _colors.scss _buttons.scss _cards.scss _forms.scss _home.scss _blog.scss _dashboard.scss style.css Compiled Output 7-1 Pattern: Scalable SASS Architecture

A simplified 7-1 pattern, a common SASS layout for large projects

main.scss: Organizing Imports
// 1. Abstracts (no output, pure utilities)
@use 'abstracts/variables';
@use 'abstracts/mixins';
@use 'abstracts/functions';

// 2. Base (reset and global defaults)
@use 'base/reset';
@use 'base/typography';
@use 'base/colors';

// 3. Layout (structural containers)
@use 'layout/grid';
@use 'layout/header';
@use 'layout/footer';
@use 'layout/sidebar';

// 4. Components (reusable UI pieces)
@use 'components/buttons';
@use 'components/cards';
@use 'components/forms';
@use 'components/modals';
@use 'components/badges';

// 5. Pages (page-specific overrides)
@use 'pages/home';
@use 'pages/blog';
@use 'pages/dashboard';

// 6. Themes
@use 'themes/dark';
@use 'themes/light';
@use vs @import

Modern SASS uses @use instead of the older @import. The @use rule is scoped and namespaced: variables from an imported file are accessed as variables.$color-primary rather than just $color-primary. That stops naming collisions in large codebases. Dart Sass has deprecated @import and plans to remove it, so new code should use @use and @forward. LESS still uses @import.

Head-to-Head Comparison

Here's how they line up on the things that tend to matter once a project gets big:

Feature SASS / SCSS LESS
Variable Syntax $variable @variable
Mixin Declaration @mixin name($args) { } .name(@args) { }
Mixin Usage @include name($args) .name(@args);
Inheritance @extend .class :extend(.class)
Control Flow @if, @for, @each, @while Guard mixins only
Custom Functions @function name() { @return val; } Limited (no true functions)
Nesting Yes, with & Yes, with &
Maps / Data Structures Yes (map-get, map-merge) Limited
Compiler Runtime Dart Sass (standalone binary) Node.js (JavaScript)
Compile Speed Very fast (Dart Sass) Fast
CSS Compatibility SCSS syntax is a strict superset of CSS Near-superset of CSS
Module System @use / @forward (namespaced) @import (flat scope)
Ecosystem / Popularity Dominant: used by Bootstrap 5, Angular, Vue CLI Declining: Bootstrap 3, Ant Design 4, legacy projects
Learning Curve Moderate Easier for beginners

Setting Up Your Workflow

Neither takes long to set up. These are the setups I see most often:

SASS with Node.js / npm

1

Install Dart Sass

Install the official Dart Sass package. Skip node-sass. It's deprecated, built on the dead LibSass engine, and its native bindings tend to break on every Node.js upgrade.

Terminal
# Install Dart Sass
npm install -D sass

# Or globally for CLI use
npm install -g sass
2

Add npm scripts

Configure build and watch commands in package.json for development and production.

package.json
{
  "scripts": {
    "sass:watch":  "sass src/scss/main.scss public/css/style.css --watch --source-map",
    "sass:build":  "sass src/scss/main.scss public/css/style.css --style=compressed --no-source-map",
    "sass:dev":    "sass src/scss/ public/css/ --watch --source-map"
  }
}
3

Integrate with Vite (recommended for modern projects)

Vite compiles SASS on its own once the sass package is installed. No config needed unless you want global variables.

Terminal: Vite + SASS
# Create Vite project
npm create vite@latest my-project -- --template vanilla

# Install sass (Vite auto-detects it)
npm install -D sass

# In your JS entry point, just import the .scss file
# import './styles/main.scss'

# Or in vite.config.js for global variables
export default {
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/abstracts/variables" as *;`
      }
    }
  }
}

LESS with Node.js

Terminal: LESS setup
# Install LESS compiler
npm install -D less

# Compile LESS to CSS
npx lessc src/less/main.less public/css/style.css

# Watch mode (using less-watch-compiler)
npm install -g less-watch-compiler
less-watch-compiler src/less/ public/css/ main.less

# With webpack
npm install -D less less-loader
# In webpack.config.js:
# { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] }

When I'd Use SASS, LESS, or Neither

Either one will handle the styling of a normal app. The real question is what your team, framework, and existing code already use.

Pick SASS/SCSS when:

  • You're starting a new project and want a preprocessor at all. SCSS is the default most tools assume
  • You're on Angular, Vue, Next.js, Laravel with Vite, or similar (all support SCSS with just the sass package)
  • You need real logic: loops, maps, custom functions, conditional styles
  • You're building a design system or component library that generates lots of variants
  • You want the widest choice of linters, docs, and Stack Overflow answers

Stick with LESS when:

  • You're maintaining a Bootstrap 3 or Ant Design 4 codebase that's built on it
  • Your project already uses LESS and migration cost outweighs the benefits
  • Your team is already proficient in LESS and the project is small to medium sized
  • You prefer simpler syntax and don't need advanced programming constructs
  • You need client-side compilation for a prototype (LESS can run in the browser via less.js, though I wouldn't ship that to production)

And sometimes the answer is neither. If your project uses Tailwind, most of your styling lives in utility classes and a preprocessor adds a build step for very little. Native CSS now has custom properties and nesting in all current browsers, so a small site may not need SASS or LESS at all.

What About CSS Custom Properties?

CSS custom properties (like --color-primary: #7c3aed) handle variables natively in the browser and can change at runtime from JavaScript, which preprocessor variables can't. They don't give you mixins, loops, or functions, though. A common setup is custom properties for runtime theming (dark mode, user themes) and SASS for build-time logic. One gotcha: SASS color functions like darken() need a real color at compile time, so darken(var(--color-primary), 10%) fails to compile.

Mistakes I See Most Often

For a new project in 2026 that wants a preprocessor, I'd pick SCSS. Any valid CSS file is already valid SCSS, so there's nothing to unlearn, and every major framework supports it out of the box. Whichever one you use, these are the problems that keep showing up:

What to check in an existing codebase

  • Nesting five levels deep. The output selectors get long and specific, and overriding them later means even longer selectors or !important.
  • Big mixins where a class would do. Every @include duplicates the block. If the styles are identical everywhere, a shared class or a placeholder with @extend produces less CSS.
  • Still on node-sass or @import. Both are on the way out. Move to the sass package and @use before a Node.js or Dart Sass upgrade forces it on you mid-release.
  • Ignoring deprecation warnings. When a build prints hundreds of them, nobody notices the one real error in the middle. Clean them up with sass-migrator.
  • Migrating LESS to SASS just because. If a LESS codebase works and the team knows it, a rewrite rarely pays for itself. Migrate when you're already doing a major redesign or dropping Bootstrap 3.
  • Using preprocessor variables for themes. They're gone after compile. Anything that has to change in the browser, like dark mode, belongs in CSS custom properties.
SASS LESS CSS SCSS Frontend Styling Preprocessors
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.