LESS to SASS Converter

Paste LESS and read SASS indented syntax beside it. Variables, nesting, guards, and mixin calls are rewritten, braces and semicolons come out, and anything LESS does that SASS has no word for shows up in the review notes instead of failing silently at compile time.

LESS to SASS conversion bench

.less to .sass
Source0 lines
Output0 lines
Variables0
Mixins0
At-rules kept0
LESS sourceEdits convert as you type
SASS outputIndented syntax, read only
Paste LESS to see the review notes

Two conversions happening in one pass

Moving a stylesheet from LESS to SASS changes two things at once, and mixing them up is where most conversions go wrong. The dialect changes, so @brand becomes $brand and a mixin stops being an ordinary class. The syntax changes as well, so braces and semicolons disappear and indentation carries the nesting. A find-and-replace gets the punctuation right and the dialect wrong, and the file dies at the first breakpoint. The bench above separates the two, and reports every construction LESS supports that SASS spells another way.

The at-sign means two different things in LESS

In LESS, @ opens a variable name. In CSS, the same character opens an at-rule. LESS lives with the collision because its parser knows @media from @brand. A converter written as one regular expression does not, and the output looks fine until a browser reaches the breakpoint.

What a naive pass writes
$brand: #2f6fb2
$import "core/reset"
$media (min-width: 62rem).btn
padding: $radius
What the file needs
$brand: #2f6fb2
@import "core/reset"
@media (min-width: 62rem).btn
padding: $radius

The converter here keeps a list of every CSS and SASS at-rule and steps over those names. Everything else with a leading @ is treated as a variable. The At-rules kept counter shows how many names survived untouched, which is the fastest way to confirm the pass understood your breakpoints.

LESSWhat it meansSASS output
@brand: #2f6fb2Variable declaration$brand: #2f6fb2
@media (min-width: 62rem)CSS at-ruleKept as written
@{brand}Name pasted into a selector or string#{$brand}
@media @bp-wideVariable holding a whole query@media #{$bp-wide}
~"(min-width: 62rem)"Escaped literalunquote("(min-width: 62rem)")
@@nameVariable named by another variableNothing equivalent, flagged
@argumentsEvery argument as one listNeeds a $args... parameter, flagged
@import (reference) "x"Import with LESS-only options@import "x", option dropped and flagged

The @media @bp-wide row is the one people miss. LESS reads a bare variable name in a media prelude and pastes its contents. SASS parses the prelude as CSS, so a bare $bp-wide sits there as literal text and the query never matches. The name has to be wrapped as #{$bp-wide}. The converter does that wrapping and says so in the notes, because the broken form still compiles without an error.

Mixins stop being selectors

LESS has no separate idea of a mixin. Any class is callable, so .truncate; on a line of its own pastes that rule set into the current block. SASS splits the two roles: @mixin declares something with no output of its own, and @include calls it. Rewriting the punctuation is easy. Deciding which role each of your classes was playing is not.

LESS formSASS formNotes
.pill(@bg; @fg: #fff) { }@mixin pill($bg, $fg: #fff)Semicolon separator becomes a comma
.pill(@brand);@include pill($brand)Direct
.truncate;@include truncateOnly works when .truncate is a mixin, not a class
.btn:extend(.card)@extend .card inside the blockMoves out of the selector
#ns > .helper();@include ns.helper()Needs @use "ns", flagged
.pill(@brand) !important;No equivalentLESS pushes the flag onto every declaration inside

The semicolon in .pill(@bg; @fg: #fff) is a LESS habit worth understanding before the converter rewrites it. LESS reads commas as list separators inside a single argument, so .shadow(0 0 2px red, 0 0 4px blue) passes one argument, and the semicolon form passes two. SASS reads commas as argument separators and treats a parenthesised group as one value, so the swap is safe in both directions. Load the sample and watch @bg and @fg land as two parameters.

The class-that-is-also-a-mixin case has no clean answer, so the bench flags it rather than guessing. When .truncate appears in your file as a rule and as a call, LESS emits .truncate to the compiled CSS and pastes its declarations at the call site. SASS refuses to do both under one name.

Pick one. Keep the class and change the call to @extend .truncate when the class is used in your HTML. Turn the rule into @mixin truncate when nothing references the class name, and accept that the standalone selector disappears from the output CSS. The notes panel names every class caught in this position.

Guards become an @if inside the mixin

A LESS guard hangs a condition off the mixin signature with when. SASS has no such position, so the condition moves inside the body as an @if, and the whole block indents one level deeper. The converter performs that reshape, which is why the output has a level of nesting the source did not.

LESS guard
.pill(@bg) when (iscolor(@bg)) {background: @bg;}
SASS equivalent
@mixin pill($bg)@if meta.type-of($bg) == color
background: $bg

The guard test functions have no SASS names, so the converter passes them through unchanged and flags the line. Swapping them is mechanical once you load sass:meta and sass:math.

LESS guard testSASS replacement
iscolor(@v)meta.type-of($v) == color
isnumber(@v)meta.type-of($v) == number
isstring(@v)meta.type-of($v) == string
ispixel(@v)math.unit($v) == "px"
ispercentage(@v)math.unit($v) == "%"
isdefined(@v)meta.variable-exists("v")
@a > 0, @b > 0$a > 0 or $b > 0
default()No equivalent, restructure the mixin

Guards do more than filter. LESS resolves a call by trying every mixin with a matching name, keeping the ones whose arity fits and whose guard passes, then merging their output. Four definitions of .button with different guards is normal LESS. SASS allows one @mixin per name, so those four have to collapse into one mixin with default arguments and an @if chain. No converter does that merge, because the merge is a design decision about which branch wins.

Rules the indented syntax enforces

SASS files carry the .sass extension, and a compiler pointed at .scss will not read them. The parser follows a shorter set of rules than the braced form, and each one shapes the output above.

Where LESS and SASS disagree about meaning

Everything above is translation. What follows is not, and no converter catches any of it, because both files parse cleanly and only the compiled CSS differs.

Variable resolution runs in opposite directions. LESS evaluates variables lazily and lets a later definition in the same scope win. SASS reads top to bottom and takes the value standing at the moment of use.

LESS gives 20px
@size: 10px;.a { width: @size; }
@size: 20px;
SASS gives 10px
$size: 10px
.a
width: $size
$size: 20px

Every LESS codebase that redefines a variable after a theme import depends on this, and the converted file silently picks the other value. Search your source for names declared more than once before you trust any converted output.

An order that keeps the diff readable

  1. Convert the variables file first. It has no mixin calls and no guards, so it lands clean, and every other file leans on the names it declares.
  2. Convert the mixin library second. This is where the guards and the arity overloads live, so it takes the longest and everything downstream depends on the decisions you make here.
  3. Convert components last, one file per pass. Read the notes panel before you read the output pane. The output pane always looks plausible.
  4. Fix flagged lines in the SASS, never in the LESS. Editing the source to make the converter happier leaves you maintaining two dialects at the same time.
  5. Diff the compiled CSS, not the source. Run lessc on the old tree and sass on the new one, push both through the same formatter, and compare. Matching CSS is the only proof the pass worked, and it catches the lazy-evaluation trap above, which reading the source never will.
  6. Delete the LESS build once the CSS matches. Keeping both alive means every new rule gets written twice, and one of the two copies starts drifting within a week.

What this converter does not attempt

Everything runs in this tab. The parser, the rewrite passes, and the review notes are JavaScript on the page, so a private theme file or an unreleased brand palette stays on your machine. Load the page once, drop the network, and the bench keeps converting.

Questions about moving LESS to SASS

At-rules, mixin roles, guards, and the differences a converter cannot fix.

Why did @media survive when @brand became $brand?

Because LESS overloads the at-sign. It opens a variable name in LESS and an at-rule in plain CSS, and the parser tells them apart by context. The converter keeps a list of every CSS and Sass at-rule, so media, supports, keyframes, font-face, container, layer, import, use, mixin, include, and the rest step past the variable rewrite untouched. Everything else with a leading at-sign is read as a variable. A converter without that list writes $media and the stylesheet loses every breakpoint while still compiling without an error.

The output has an @include for a class I can see in the same file. Why is that flagged?

Because LESS lets one name be a class and a mixin at the same time, and Sass does not. A rule called .truncate in LESS compiles to a .truncate selector in the CSS and also pastes its declarations wherever .truncate; appears. Sass makes you pick. Keep the class and change the call to @extend .truncate when your HTML uses the class name. Turn the rule into @mixin truncate when nothing references it, and the standalone selector disappears from the compiled output. The notes panel lists every name caught in both roles.

My media query stopped matching after conversion. What happened?

A variable holding a query needs wrapping. LESS accepts @media @bp-wide and pastes the stored text into the prelude. Sass parses the prelude as CSS, so a bare $bp-wide sits there as literal text and no device ever matches it. The correct form is @media #{$bp-wide}. This converter adds the interpolation and reports it, since the broken version compiles quietly and only fails in the browser.

How do LESS guards translate?

A guard hangs a condition off the mixin signature with the word when. Sass has no such position, so the condition moves into the mixin body as an @if and the block indents one level deeper. The converter performs that reshape. Guard test functions such as iscolor and ispixel have no Sass names, so they pass through and get flagged. Replace iscolor with meta.type-of comparison against color, and ispixel with math.unit compared to the string px, after loading sass:meta and sass:math.

Can I convert several mixins that share one name?

The pass converts them, and the result will not compile. LESS resolves a call by matching every mixin of that name whose argument count fits and whose guard passes, then merging their output. Sass allows one definition per name. Collapse the set into a single mixin with default argument values and an @if chain inside. That merge is a design decision about which branch wins, so it stays a manual step rather than a guess the tool makes for you.

Is SASS indented syntax the same thing as SCSS?

Same compiler and same feature set, different punctuation. SCSS uses braces and semicolons and is a superset of CSS, so plain CSS pastes into it. The indented syntax drops both and uses whitespace for nesting, with the .sass extension. Choose SCSS when your team copies CSS in from other places or reads a lot of third-party examples. Choose the indented syntax when you want shorter files and a parser that refuses inconsistent indentation. Converting between the two later is mechanical.

Why is a value spread over three lines in my source now one long line?

The indented syntax has no line continuation. A declaration ends where the line ends, so a box-shadow list or a grid-template-columns value written across several source lines has to be joined into one. The converter joins them rather than emitting something that fails to parse. Store the value in a variable when the line becomes hard to read, then reference the variable in the declaration.

Does the converter compile the LESS or evaluate any expressions?

No. It rewrites text and reports what it changed. Arithmetic stays as arithmetic, colour functions keep their arguments, and nothing on disk is read. That matters most around division, since LESS treats a slash between two values as division and Dart Sass treats it as a separator. Any slash beside a variable is flagged so you can wrap it in math.div after loading sass:math.

How do I check the conversion actually worked?

Compile both trees and diff the CSS rather than reading the source. Run lessc over the old files and the sass binary over the new ones, push both results through the same formatter, then compare. Matching CSS is the only real proof. Reading the source misses the difference that bites hardest, which is variable resolution order. LESS lets a later declaration in the same scope win, while Sass takes the value in place at the moment of use, so a file that redefines a name after a theme import gives different numbers in each language.

Is my stylesheet uploaded anywhere?

No. Parsing, the rewrite passes, and the review notes all run in JavaScript inside this page. No request goes out after the page loads and nothing is stored between visits. Closing the tab clears both panes, so an unreleased theme or a client palette under embargo stays on your machine.