LESS to SCSS Converter

SCSS keeps the braces and the semicolons, so a converted LESS file looks finished the moment the at-signs turn into dollar signs. Paste your source and read the ledger under the panes, which names every line rewritten cleanly, every line worth a second look, and every construction Sass has no word for.

LESS to SCSS conversion bench

.less to .scss
LESS sourceEdits convert as you type
SCSS outputRead only
0 variable references renamed.

A file compiling is not a file converting

LESS and SCSS read like siblings. Braces stay, semicolons stay, nesting stays, both comment styles work in each. Swap every @ for a $ and the Sass compiler stops complaining, which is where the trouble starts. Sass parses most LESS habits without an error, then writes different CSS from them, so a bad conversion reaches the browser instead of the terminal. The bench above sorts its own work into three groups, because the group a line falls into tells you whether you have to read it.

GroupWhat the pass didWhat you do with it
RewrittenAn exact swap with the same meaning on both sidesSkim it
CheckRewritten, with a behaviour difference sitting behind the new spellingRead the note, then diff the compiled CSS
BlockedA LESS construction with no SCSS spelling, left exactly as writtenRedesign that part before the file builds

The at-sign carries three jobs in LESS

In LESS the @ opens a variable name, opens an at-rule, and marks interpolation as @{...}. Sass spreads those across two characters: $ for a name, #{...} for interpolation, @ reserved for at-rules alone. One regular expression over the file gets a third of the job right and breaks the rest.

LESSWhat LESS reads thereSCSS
@radius: 6px;Variable declaration$radius: 6px;
@media (min-width: 62rem)CSS at-ruleKept as written
@{state}A name pasted into a selector, a property, or a string#{$state}
@media @bp-wideA variable holding a whole query@media #{$bp-wide}
~"(min-width: 62rem)"Escaped literalunquote("(min-width: 62rem)")
@@sideThe variable named by another variableNo equivalent, use a map
@import (reference) "x"Import with LESS-only options@use "x", options dropped
@plugin "x"JavaScript running inside the compilerNothing, delete the line

The fourth row is the one that ships broken. Watch what a careless rename does to a stored breakpoint.

LESS source
@bp-wide: ~"(min-width: 62rem)";@media @bp-wide {.card { padding: 32px; }}
SCSS after a plain rename
$bp-wide: "(min-width: 62rem)";@media $bp-wide {.card { padding: 32px; }}
CSS the browser receives
@media $bp-wide {.card { padding: 32px; }}

No error, no warning, no matching device. Sass parses a media prelude as CSS text, finds nothing to resolve in $bp-wide, prints it, and moves on. The fix is @media #{$bp-wide}, which is why this converter keeps a list of every CSS and Sass at-rule and steps over those names rather than treating a leading at-sign as proof of a variable.

Every class in LESS is already a mixin

LESS never separated the two ideas. Write .truncate { ... } and you own a class in the compiled CSS plus a mixin callable as .truncate; from any block. Sass holds the roles apart with @mixin and @include, so a converter has to decide which role each of your rules was playing, and for some rules both answers are correct.

LESS formSCSS formWhat moved
.pill(@bg; @fg: #fff) { }@mixin pill($bg, $fg: #fff) { }The semicolon separator becomes a comma
.pill(#2f6fb2);@include pill(#2f6fb2);Direct, no judgement needed
.truncate;@include truncate;Correct only when the name is a mixin, not a class your markup uses
.btn:extend(.card)@extend .card; inside the blockOff the selector, into the body
.btn:extend(.card all)@extend .card;The all keyword has nowhere to go
#ns > .helper();@include ns.helper();Needs the block moved into its own partial
.pill(@brand) !important;No equivalentLESS pushed the flag onto every declaration inside

One entry earns a second read. LESS resolves .pill(...) by collecting every definition of that name whose argument count fits and whose guard passes, then merging their output. Four definitions of .button under four guards is ordinary LESS.

Sass allows one mixin per name, and a second @mixin pill raises no error at all. It replaces the first one, and every call from that line onward takes the new body. The ledger marks a repeated name as blocked because the repair is a design decision: collapse the set into one mixin with default values and an @if chain, then pick which branch wins when two conditions both hold.

Guards move inside the mixin

A LESS guard hangs its condition off the signature with when. Sass has no such position, so the test lands on the first line of the body as an @if and the block gains a level of indentation. The guard test functions carry no Sass names, so each one becomes a module call.

LESS guard testSCSS 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, write the else branch yourself

meta and math are modules, so a file with converted guards needs @use "sass:meta"; above its first rule. The bench adds those load lines when the output reaches for them and records each one in the ledger, so a line you did not write never appears without a reason attached.

Shared names, different arithmetic

This table is where a converted file compiles cleanly and prints other numbers. Every row below survives the Sass parser.

CallWhat LESS doesWhat Sass does
unit(5, px)Attaches the unit, gives 5pxmath.unit(5px) reads a unit back, giving the string "px"
@gutter / 2DivisionA slash separator, with division removed from the operator
fade(@c, 25%)Sets alpha to 0.25rgba($c, 0.25)
fadeout(@c, 10%)Lowers alpha by 0.1color.adjust($c, $alpha: -0.1)
tint(@c, 10%)Mixes toward whitemix(#fff, $c, 10%)
greyscale(@c)Drops saturationgrayscale($c), one letter apart
darken(@c, 12%)Subtracts lightnessStill resolves, now deprecated for color.adjust()
contrast(@c, dark, light)Picks a colour against a thresholdNo equivalent, write the choice out
data-uri("logo.svg")Opens the file at compile timeNo file access of any kind

The first row is the sharpest one on this page. unit() exists in both languages under the same name and does opposite jobs. LESS attaches a unit to a bare number. Sass reads the unit off a number and hands back a string. A line like font-size: unit(15, px); converts to a call Sass resolves without complaint, and the declaration lands in your CSS as a quoted string instead of a size.

The order your declarations run in

LESS resolves a variable lazily, so the last declaration in a scope wins everywhere in that scope, including on the lines above itself. Sass reads top to bottom and uses the value standing at the moment of use. Same names, same file, different CSS.

LESS source
@gutter: 24px;.card { padding: @gutter; }
@gutter: 32px;
SCSS after the rename, line for line
$gutter: 24px;.card { padding: $gutter; }
$gutter: 32px;
Compiled CSS from each
lessc .card { padding: 32px; }
sass .card { padding: 24px; }

Any theme file that redefines a token after an import leans on the LESS behaviour, and the converted file quietly takes the other value. Search your source for names declared more than once before you trust the output. The ledger flags each repeat and names the line the earlier declaration sat on.

Loading a file stops being a paste

@import in LESS pastes the file in at that spot. Sass had the same behaviour under the same keyword and is retiring it. @use loads a file once, namespaces what the file declares, and keeps private names private, so the load line is the part of a conversion with the most real work behind it.

A conversion order that keeps the diff small

File order decides how much of the work you do twice. Convert the files nothing depends on last.

StageWhat you convertThe failure to expect
1Tokens and breakpointsA stored query string losing its interpolation
2The mixin libraryTwo mixins under one name, where the second wins in silence
3Shared partials and namespacesA helper reached through #ns > .helper() that now needs a file of its own
4Components, one file per passA bare .name; call that turns out to be a class your markup uses
5VerificationA value that moved because a token was declared twice

Compile both trees and diff the CSS rather than reading the source. Run lessc over the old files and sass over the new ones, push both results through the same formatter, then compare. Matching CSS is the only proof the pass worked, and source review misses the two failures with the longest tail: the redeclared token and the overwritten mixin name.

What this converter leaves for you

Everything runs in this tab. The parser, the rewrite passes, and the ledger are JavaScript on the page, so an unreleased theme or a client palette under embargo stays on your machine. Load the page once, drop the network, and the bench keeps converting.

Questions about moving LESS to SCSS

At-signs, mixin roles, guards, module loading, and the differences a converter cannot repair for you.

The SCSS output looks almost identical to my LESS. Did anything actually happen?

That similarity is the reason this particular conversion catches people out. SCSS keeps braces, semicolons, nesting, the ampersand parent selector, and both comment styles, so most of the punctuation you are looking at is meant to survive untouched. Read the ledger rather than the output pane. It lists every line the pass rewrote, and the counters tell you how many at-signs became dollar signs. If the ledger holds nothing but Rewritten entries, the file really was a plain dialect swap. If it holds Check or Blocked entries, those lines compile and behave differently, which is the part the output pane cannot show you.

My breakpoints stopped matching after conversion. What broke?

A variable holding a whole media query needs interpolation in Sass. LESS accepts @media @bp-wide and pastes the stored text into the prelude. Sass parses a prelude as CSS, so a bare $bp-wide sits there as literal text, no device ever matches it, and the compiler prints the rule without a warning. The correct form is @media #{$bp-wide}. This converter adds the interpolation and records it in the ledger, because the broken version is valid Sass and only fails in a browser.

I have four mixins sharing one name. What does SCSS do with them?

It keeps the last one and drops the rest without saying so. LESS resolves a call by matching every definition of that name whose argument count fits and whose guard passes, then merging their output, which is why an overload set is normal in LESS. Sass allows one mixin per name and treats a second declaration as a redefinition rather than an error. Collapse the set into a single mixin with default argument values and an @if chain inside, and decide up front which branch wins when two guards both pass. The ledger marks every repeated name as Blocked for exactly this reason.

Why is unit() flagged when both languages have the function?

Because they share the name and do opposite jobs. LESS unit(5, px) attaches a unit to a bare number and returns 5px. Sass math.unit(5px) reads the unit off a number and returns the string "px". A declaration written as font-size: unit(15, px) survives the conversion, resolves in Sass without complaint, and lands in your CSS as a quoted string where a size belongs. Multiply by the unit instead, so 15 * 1px, or store the value with its unit from the start.

Why did my division turn into math.div?

LESS reads a slash between two values as division. Dart Sass reads a slash as a separator and removed the division behaviour after a long deprecation, so $gutter / 2 in Sass produces the two values with a slash between them rather than half the gutter. math.div($gutter, 2) is the replacement, and it needs @use "sass:math" at the top of the file, which the bench adds when the output reaches for it. Turn the toggle off if you would rather see the slashes flagged and fix them by hand.

Should the converted file use @import or @use?

@use, in almost every case. Sass @import behaves like the LESS one, pasting the file in and dropping every name into a single shared pool, and it is on its way out of the language. @use loads a file once however many places ask for it, which is the job LESS did with the (reference) and (once) import options, and it namespaces what the file declares so $brand from a tokens partial reads as tokens.$brand. Rename each partial to start with an underscore, drop the extension from the load line, and put those lines above every rule in the file.

The pass turned .truncate; into @include truncate;. Is that right?

Only if .truncate was a mixin. LESS lets one name be a class and a mixin at once, so a rule called .truncate emits a .truncate selector into the compiled CSS and also pastes its declarations wherever .truncate; appears. Sass makes you choose. Keep the class and change the call to @extend .truncate when your HTML uses that class name. Turn the rule into @mixin truncate when nothing references it, and accept that the standalone selector disappears from the output. Every bare call is marked Check so you can decide per name.

Does :extend map onto @extend exactly?

Close enough to convert, not close enough to skip the diff. Both pull the extending selector into the extended rule rather than copying declarations. The position differs, since LESS hangs :extend off the selector and Sass puts @extend inside the block, and the LESS all keyword has no Sass counterpart because Sass extends already reach compound and nested uses. Where the two drift is selector ordering and the handling of an extend inside a media query, so compile both trees and compare the CSS for any rule that uses either form.

Is SCSS a superset of LESS?

No, and the assumption behind the question is what makes this conversion slippery. SCSS is a superset of CSS, which means any valid CSS file is a valid SCSS file. LESS is a separate language that also happens to look like CSS with braces. Where the two overlap you get free passage, which covers nesting, the ampersand, media nesting, and comments. Where they disagree, on variables, mixins, guards, imports, and around a dozen function names, Sass reads your LESS as something else rather than rejecting it.

Does my stylesheet leave the browser?

No. Parsing, the rewrite passes, and the ledger all run in JavaScript inside this page. No request goes out after the page has loaded, nothing is stored between visits, and closing the tab clears both panes. A private theme file, a client palette under embargo, or an unreleased brand colour stays on your machine.