CSS Preprocessors: SASS vs LESS
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.
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:
- Variables: Store colors, fonts, and other values in reusable variables
- Nesting: Nest selectors to reflect your HTML structure more clearly
- Mixins: Define reusable blocks of styles, similar to functions
- Partials & Imports: Split your CSS into modular files and import them
- Functions & Operations: Perform math and logic directly in your stylesheets
- Inheritance / Extend: Share style sets between selectors
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.
// === 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.
// === 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;
}
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 &:
// 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 { ... } ... }
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
// 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.)
// 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.
// 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;
}
}
// 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;
}
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:
A simplified 7-1 pattern, a common SASS layout for large projects
// 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';
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
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.
# Install Dart Sass
npm install -D sass
# Or globally for CLI use
npm install -g sass
Add npm scripts
Configure build and watch commands in package.json for development and production.
{
"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"
}
}
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.
# 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
# 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
sasspackage) - 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.
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
@includeduplicates the block. If the styles are identical everywhere, a shared class or a placeholder with@extendproduces less CSS. - Still on
node-sassor@import. Both are on the way out. Move to thesasspackage and@usebefore 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.