← Back to DevBytes

Migrating from Sass to Less: Step-by-Step Guide

Introduction: Understanding Sass and Less

Both Sass and Less are powerful CSS preprocessors that extend CSS with variables, nesting, mixins, functions, and more. While they share many conceptual similarities, their syntax and feature implementations differ significantly. Migrating from Sass to Less involves translating your existing .scss files into .less files while preserving the same functionality and output. This step-by-step guide walks you through every aspect of the migration process, from variable declarations to complex mixins and loops.

Why Migrate from Sass to Less?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Several practical reasons might drive this decision:

Key Differences Overview

Before diving into migration, understand the fundamental syntactic and conceptual differences between the two preprocessors:

Step 1: Setting Up Your Less Environment

Before converting files, install Less and configure your build process. The most common approach uses Node.js:

npm install less --save-dev

For a basic project, create a directory structure to organize your Less files:

styles/
├── less/
│   ├── variables.less
│   ├── mixins.less
│   ├── base.less
│   └── main.less
├── css/
│   └── (compiled output)
└── package.json

Configure a build script in package.json to compile Less automatically:

{
  "scripts": {
    "build:css": "lessc styles/less/main.less styles/css/main.css",
    "watch:css": "lessc --watch styles/less/main.less styles/css/main.css"
  }
}

Step 2: Converting Variables

This is the most mechanical part of the migration. Sass variables use the dollar sign ($), while Less uses the at-sign (@). Simply replace all variable declarations and references throughout your files.

Sass Variable Declaration (Original)

// variables.scss
$primary-color: #3498db;
$font-stack: 'Helvetica Neue', Helvetica, Arial, sans-serif;
$base-padding: 16px;
$border-radius: 4px;

.card {
  background-color: $primary-color;
  font-family: $font-stack;
  padding: $base-padding;
  border-radius: $border-radius;
}

Less Variable Declaration (Converted)

// variables.less
@primary-color: #3498db;
@font-stack: 'Helvetica Neue', Helvetica, Arial, sans-serif;
@base-padding: 16px;
@border-radius: 4px;

.card {
  background-color: @primary-color;
  font-family: @font-stack;
  padding: @base-padding;
  border-radius: @border-radius;
}

Important Note on Variable Scoping

Less variables use lazy loading, meaning the last definition wins regardless of scope order. Sass variables are immediately resolved. This can cause subtle differences if you rely on scoped variable overrides. Always place your variable definitions at the top of their respective scope in Less to maintain predictable behavior.

// In Less, this works due to lazy loading
.box {
  width: @size; // @size is resolved from the later definition
  @size: 100px;
}

// In Sass, this would have thrown an error or used an earlier definition

Step 3: Translating Mixins

Mixins represent the most substantial conceptual shift. Sass uses a dedicated @mixin/@include pattern with explicit parameter syntax. Less mixins are simply class selectors that can be invoked by referencing their name, and they output their properties unless you append parentheses to make them "parametric" (non-outputting) mixins.

Simple Mixin Without Parameters

Sass:

@mixin clearfix {
  &::after {
    content: '';
    display: table;
    clear: both;
  }
}

.container {
  @include clearfix;
}

Less (parametric mixin with empty parentheses prevents output of the mixin itself):

.clearfix() {
  &::after {
    content: '';
    display: table;
    clear: both;
  }
}

.container {
  .clearfix();
}

Mixins with Parameters and Default Values

Sass:

@mixin button-style($bg-color, $text-color: white, $padding: 10px 20px) {
  background-color: $bg-color;
  color: $text-color;
  padding: $padding;
  border-radius: 4px;
  cursor: pointer;
}

.primary-btn {
  @include button-style(#3498db);
}

.danger-btn {
  @include button-style(#e74c3c, white, 12px 24px);
}

Less:

.button-style(@bg-color, @text-color: white, @padding: 10px 20px) {
  background-color: @bg-color;
  color: @text-color;
  padding: @padding;
  border-radius: 4px;
  cursor: pointer;
}

.primary-btn {
  .button-style(#3498db);
}

.danger-btn {
  .button-style(#e74c3c, white, 12px 24px);
}

Variable Arguments / Rest Parameters

Sass uses $args...:

@mixin transition($properties...) {
  transition: $properties;
}

.element {
  @include transition(all 0.3s ease, opacity 0.2s);
}

Less uses @arguments or rest parameters with ...:

.transition(@properties...) {
  transition: @properties;
}

.element {
  .transition(all 0.3s ease, opacity 0.2s);
}

Step 4: Converting Extend / Inheritance

Sass uses the @extend directive to share a set of CSS properties between selectors. Less provides the &:extend() pseudo-selector function, which offers more granular control over how and where the extension is applied.

Basic Extend

Sass:

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

.message-success {
  @extend .message;
  border-color: green;
}

.message-error {
  @extend .message;
  border-color: red;
}

Less:

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

.message-success {
  &:extend(.message);
  border-color: green;
}

.message-error {
  &:extend(.message);
  border-color: red;
}

Extending Multiple Classes and Using the all Keyword

Less's &:extend() can target multiple selectors and, with the all keyword, extend every instance of a selector including nested occurrences:

.base-style {
  color: #333;
  font-size: 14px;
}
.base-style .nested {
  margin-left: 20px;
}

.derived {
  &:extend(.base-style all);
  font-weight: bold;
}
// This extends both .base-style and .base-style .nested

In Sass, @extend .base-style does not automatically extend nested selectors; you would need additional @extend calls or use placeholder selectors.

Step 5: Handling Conditionals

Sass provides @if, @else if, and @else directives for conditional logic. Less does not have explicit conditional statements. Instead, you use guarded mixins — mixins with a when clause that acts as a condition check.

Sass Conditional Logic

@mixin theme-color($theme) {
  @if $theme == 'dark' {
    background-color: #333;
    color: #fff;
  } @else if $theme == 'light' {
    background-color: #f5f5f5;
    color: #333;
  } @else {
    background-color: #999;
    color: #000;
  }
}

.sidebar {
  @include theme-color('dark');
}

Less Guarded Mixin Equivalent

.theme-color(@theme) when (@theme = 'dark') {
  background-color: #333;
  color: #fff;
}

.theme-color(@theme) when (@theme = 'light') {
  background-color: #f5f5f5;
  color: #333;
}

.theme-color(@theme) when (default()) {
  background-color: #999;
  color: #000;
}

.sidebar {
  .theme-color('dark');
}

The when (default()) guard serves as the fallback/default case, similar to Sass's @else. Guards can use comparison operators (>, <, >=, <=, =) and logical operators (and, or, not).

Advanced Guard Example

.responsive-width(@size) when (@size >= 768px) and (@size <= 1200px) {
  width: 75%;
}

.responsive-width(@size) when (@size > 1200px) {
  width: 60%;
}

.responsive-width(@size) when (@size < 768px) {
  width: 100%;
}

.content {
  .responsive-width(1024px);
}

Step 6: Converting Loops

Sass supports @for, @each, and @while loops. Less requires recursive mixins to achieve iteration. This is one of the more challenging aspects of migration, as you need to restructure your looping logic entirely.

Sass @for Loop (Through a Numeric Range)

// Generate margin classes: .mt-1 through .mt-5
@for $i from 1 through 5 {
  .mt-#{$i} {
    margin-top: #{$i * 8}px;
  }
}

Less Recursive Mixin Equivalent

// Generate margin classes: .mt-1 through .mt-5
.generate-margin(@i) when (@i <= 5) {
  .mt-@{i} {
    margin-top: (@i * 8px);
  }
  .generate-margin(@i + 1);
}

// Start the recursion
.generate-margin(1);

Notice the pattern: the mixin calls itself with an incremented value until the guard condition fails. This is the standard Less approach for all iteration.

Sass @each Loop (Iterating Over a List)

$colors: ('red', 'blue', 'green', 'yellow', 'purple');

@each $color in $colors {
  .bg-#{$color} {
    background-color: #{$color};
  }
}

Less List Iteration Using Recursion and extract()

@colors: red, blue, green, yellow, purple;

.generate-colors(@index) when (@index > 0) {
  @color: extract(@colors, @index);
  .bg-@{color} {
    background-color: @color;
  }
  .generate-colors(@index - 1);
}

// Get list length and start recursion
.generate-colors(length(@colors));

The extract() function retrieves a list item by its index. Combined with length(), you can iterate over any Less list.

Sass @while Loop

$level: 1;
@while $level <= 4 {
  .depth-#{$level} {
    z-index: #{$level * 100};
    padding-left: #{$level * 10}px;
  }
  $level: $level + 1;
}

Less Recursive Mixin Equivalent for While Loop

.generate-depth(@level) when (@level <= 4) {
  .depth-@{level} {
    z-index: (@level * 100);
    padding-left: (@level * 10px);
  }
  .generate-depth(@level + 1);
}

.generate-depth(1);

Step 7: Converting Built-in Functions

Both preprocessors offer extensive built-in function libraries, but their naming and syntax differ. Here are the most common function mappings you'll need:

Color Manipulation Functions

Sass FunctionLess EquivalentNotes
lighten($color, 20%)lighten(@color, 20%)Same function name
darken($color, 15%)darken(@color, 15%)Same function name
rgba($color, 0.5)fade(@color, 50%)Less uses fade() with percentage
mix($c1, $c2, 50%)mix(@c1, @c2, 50%)Same function name
saturate($color, 30%)saturate(@color, 30%)Same function name
desaturate($color, 20%)desaturate(@color, 20%)Same function name

String and Number Functions

// Sass: percentage, round, ceil, floor, abs
// Less: percentage(), round(), ceil(), floor(), abs() — largely the same

// Sass: str-length(), str-index(), to-upper-case()
// Less: length() for strings/lists, replace(), escape()

// Sass interpolation: #{$variable}
// Less interpolation: @{variable}

// Example: Sass interpolation in selectors
.my-class-#{$modifier} { }

// Equivalent Less interpolation in selectors
.my-class-@{modifier} { }

Custom Functions

Sass allows you to define custom functions in Sass itself (or via the API). Less does not support user-defined functions in the same way; instead, you use parametric mixins that compute and return a value via a temporary variable:

// Sass custom function approach
@function rem($px-value) {
  @return #{$px-value / 16}rem;
}

.element {
  font-size: rem(24); // Outputs: 1.5rem
}

// Less approach: use a mixin that sets a variable
.rem(@px-value) {
  @result: (@px-value / 16rem);
}

.element {
  .rem(24);
  font-size: @result; // Outputs: 1.5rem
}

This pattern is verbose but works for complex calculations that need to be reused.

Step 8: Converting Imports and Partials

Both Sass and Less support splitting code across multiple files. Sass has modernized its module system with @use and @forward (replacing the older @import), while Less continues to rely on @import with a reference option.

Sass Modern Import System

// main.scss (modern Sass)
@use 'variables';
@use 'mixins';
@use 'components/buttons';

// Older Sass @import approach (still supported in many versions)
@import 'variables';
@import 'mixins';
@import 'components/buttons';

Less Import System

// main.less
@import 'variables';
@import 'mixins';
@import 'components/buttons';

Less also offers @import (reference) which imports a file but does not output its CSS — useful for importing mixins and variables only:

@import (reference) 'mixins'; // Mixins available but not compiled to CSS
@import (optional) 'external-library'; // No error if file is missing
@import (multiple) 'theme'; // Allows importing the same file multiple times

Step 9: Handling Sass Placeholder Selectors

Sass placeholder selectors (prefixed with %) define reusable blocks of styles that are never output on their own — they are only emitted when extended via @extend. Less does not have a direct equivalent, but you can simulate them using parametric mixins (with empty parentheses) combined with &:extend().

Sass Placeholder Approach

%flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

.header {
  @extend %flex-center;
  height: 60px;
}

.footer {
  @extend %flex-center;
  height: 40px;
}

Less Simulation

.flex-center() {
  display: flex;
  justify-content: center;
  align-items: center;
}

.header {
  .flex-center();
  height: 60px;
}

.footer {
  .flex-center();
  height: 40px;
}

The parametric mixin .flex-center() (with parentheses) does not generate any CSS output on its own, functioning similarly to a Sass placeholder. The styles are inlined wherever the mixin is called. If you want true extension behavior (grouping selectors), combine with &:extend():

.flex-center-base {
  display: flex;
  justify-content: center;
  align-items: center;
}

.header {
  &:extend(.flex-center-base);
  height: 60px;
}

.footer {
  &:extend(.flex-center-base);
  height: 40px;
}
// Output groups: .flex-center-base, .header, .footer { ... }

Step 10: Converting Sass Maps to Less

Sass maps (key-value data structures) have no direct Less equivalent. You'll need to refactor map-based logic into lists, guarded mixins, or configuration variables.

Sass Map Usage

$breakpoints: (
  'small': 576px,
  'medium': 768px,
  'large': 992px,
  'xlarge': 1200px
);

@mixin respond-to($bp) {
  @if map-has-key($breakpoints, $bp) {
    @media (min-width: map-get($breakpoints, $bp)) {
      @content;
    }
  }
}

.sidebar {
  @include respond-to('medium') {
    width: 300px;
  }
}

Less Refactored Version Using Guarded Mixins

@small: 576px;
@medium: 768px;
@large: 992px;
@xlarge: 1200px;

.respond-to(@bp) when (@bp = 'small') {
  @media (min-width: @small) {
    @rules();
  }
}

.respond-to(@bp) when (@bp = 'medium') {
  @media (min-width: @medium) {
    @rules();
  }
}

.respond-to(@bp) when (@bp = 'large') {
  @media (min-width: @large) {
    @rules();
  }
}

.sidebar {
  .respond-to('medium');
  @rules: {
    width: 300px;
  };
}

Alternatively, for simpler key-value lookups, use a list of pairs with extract():

@breakpoint-names: small, medium, large, xlarge;
@breakpoint-values: 576px, 768px, 992px, 1200px;

.get-breakpoint(@name) {
  @index: 0;
  // Manual lookup — Less has no built-in indexOf
}

// For most cases, separate variables or guarded mixins are cleaner

Step 11: Converting Nesting and Parent Selector References

Nesting syntax is nearly identical between Sass and Less. Both use the & symbol to reference the parent selector. However, there are subtle differences in how pseudo-classes and compound selectors are handled.

Basic Nesting (Same in Both)

// Works identically in Sass and Less
.nav {
  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  li {
    display: inline-block;
  }
  a {
    text-decoration: none;
    &:hover {
      color: red;
    }
  }
}

Compound Selector Differences

In Less, & represents the entire parent selector path. When you use &-suffix, it appends directly to the parent:

// Less
.btn {
  color: blue;
  &-primary {
    color: white;
    background: blue;
  }
}
// Outputs: .btn-primary { ... } — correct compound selector

// Sass
.btn {
  color: blue;
  &-primary {
    color: white;
    background: blue;
  }
}
// Same output in Sass: .btn-primary { ... }

Both handle & consistently in this regard. The migration is straightforward — copy nesting structures as-is.

Step 12: Converting Interpolation

Interpolation embeds variable values into selectors, property names, or strings. The syntax differs between the two preprocessors.

Sass Interpolation: #{ }

$component: 'card';

.#{$component} {
  .#{$component}__title {
    font-size: 18px;
  }
  .#{$component}__body {
    padding: 10px;
  }
}

// Also works for property names
$property: 'margin';
.element {
  #{$property}-top: 10px;
}

Less Interpolation: @{ }

@component: 'card';

.@{component} {
  .@{component}__title {
    font-size: 18px;
  }
  .@{component}__body {
    padding: 10px;
  }
}

// Property name interpolation in Less
@property: 'margin';
.element {
  @{property}-top: 10px;
}

Step 13: Working with Math Operations

Both preprocessors support arithmetic operations, but Less is more lenient with unit handling. In Sass, you must ensure compatible units or use explicit conversion. Less automatically handles some unit conversions.

// Both support: +, -, *, /, and parentheses
// Less example:
@base: 16px;
.element {
  width: (@base * 2);        // 32px
  height: (@base + 4px);     // 20px
  margin: (@base / 2);       // 8px
}

// Less allows some mixed-unit operations that Sass would flag:
.box {
  width: (100% - 40px);      // Less handles this; Sass may require calc()
}

// For division in Less, use parentheses to avoid ambiguity:
.ratio {
  font-size: (16px / 1.5);   // 10.666...px
}

Step 14: Converting Sass's @content Block to Less

Sass mixins can accept a block of arbitrary styles via @content, which is passed when calling the mixin. Less does not have a direct @content equivalent. You can simulate this using a mixin that accepts a ruleset variable, but this requires restructuring the calling code.

Sass @content Usage

@mixin media-small {
  @media (max-width: 600px) {
    @content;
  }
}

.container {
  width: 80%;
  @include media-small {
    width: 100%;
    padding: 0;
  }
}

Less Workaround Using Detached Rulesets

.media-small(@rules) {
  @media (max-width: 600px) {
    @rules();
  }
}

.container {
  width: 80%;
  .media-small({
    width: 100%;
    padding: 0;
  });
}

Detached rulesets (assigned to variables or passed as arguments) can be invoked with () to output their contents. This pattern closely mimics Sass's @content functionality.

Step 15: Full File Conversion Walkthrough

Let's convert a complete, realistic Sass component file to Less, demonstrating all the transformations together.

Original Sass File (_button.scss)

// Button component — Sass version
$btn-padding: 10px 20px;
$btn-border-radius: 4px;
$btn-colors: (
  'primary': #0066cc,
  'secondary': #6c757d,
  'danger': #dc3545
);

@mixin button-base {
  display: inline-block;
  padding: $btn-padding;
  border-radius: $btn-border-radius;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}

@mixin button-variant($bg, $text: white) {
  background-color: $bg;
  color: $text;
  border: 1px solid darken($bg, 10%);

  &:hover {
    background-color: darken($bg, 8%);
    border-color: darken($bg, 15%);
  }

  &:active {
    background-color: darken($bg, 15%);
  }
}

.btn {
  @include button-base;

  @each $name, $color in $btn-colors {
    &-#{$name} {
      @include button-variant($color);
    }
  }
}

Converted Less File (button.less)

// Button component — Less version
@btn-padding: 10px 20px;
@btn-border-radius: 4px;

// Simulate the color map with individual variables
@btn-primary: #0066cc;
@btn-secondary: #6c757d;
@btn-danger: #dc3545;

@btn-color-names: 'primary', 'secondary', 'danger';
@btn-color-values: @btn-primary, @btn-secondary, @btn-danger;

.button-base() {
  display: inline-block;
  padding: @btn-padding;
  border-radius: @btn-border-radius;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}

.button-variant(@bg, @text: white) {
  background-color: @bg;
  color: @text;
  border: 1px solid darken(@bg, 10%);

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

  &:active {
    background-color: darken(@bg, 15%);
  }
}

// Generate color variants using recursion
.generate-btn-variants(@index) when (@index > 0) {
  @name: extract(@btn-color-names, @index);
  @color: extract(@btn-color-values, @index);

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

  .generate-btn-variants(@index - 1);
}

.btn {
  .button-base();
}

.generate-btn-variants(length(@btn-color-names));

This conversion demonstrates variable translation, mixin restructuring, map-to-list conversion, and recursive iteration — all core patterns you'll encounter in a real migration.

Best Practices for a Smooth Migration

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles