SCSS to LESS Converter

This is the lossy direction. Paste SCSS, watch variables and mixins rename, then read the three scores beside the LESS pane. Anything Sass grew after LESS stopped matching has no spelling on the right, so those lines stay put and get named.

SCSS to LESS conversion desk

Load
SCSS you still compileEdits rewrite as you type
LESS the desk producedRead only

You are shrinking a language, not flipping a switch

People treat a SCSS to LESS converter as the inverse of the LESS to SCSS pass. I do not. Sass kept adding nouns after LESS froze a large part of its language. Maps. Placeholder selectors. A module system with @use and @forward. Functions that @return a value. @each over a list. @at-root. The braces look the same, so a file feels finished the moment $ becomes @. Then lessc dies, or worse, prints CSS nobody asked for.

A pass that only swaps the sigil is a bug. The desk above rewrites the constructions LESS still has a word for, then leaves the rest in the output so the hole is visible.

Read the leftover score before you paste the result into a pull request. This desk never evaluates colour math, never follows an @use path, never invents a LESS form for a Sass map. A leftover line stays written as Sass on purpose. Guessing would hide the work you still have to do.

Still shared

  • Nesting, the & parent selector, both comment styles
  • Variables, once the sigil moves
  • Mixins, once the keyword drops
  • Interpolation inside selectors, properties, and strings
  • @media nesting, @keyframes, @supports
  • Extend, hanging off a selector in LESS and sitting inside the block in Sass

Gone. Do not wait for a spelling.

  • Maps, map.get, map-merge
  • Placeholder selectors starting with %
  • @use namespaces and @forward
  • User @function with @return
  • @each, @for, @while
  • @at-root, mixin @content blocks

Two dollars, two jobs

Sass $brand dies at compile time. CSS --brand lives in the browser. Plenty of current SCSS files mix both, putting a Sass token into a custom property so runtime theming still works. Touch the -- name and you break the theme. Touch only the Sass value.

:root { --brand: $brand; } should land as :root { --brand: @brand; }. The custom property keeps its dashes. Only the preprocessor value changes sigil.

Interpolation is the other dollar, and the one a careless rename wrecks. #{$state} is not a variable use in the Sass grammar. LESS wants @{state}. Leave the hash form in a LESS file and the compiler prints the characters into your selector.

SCSS you meant
$bp-wide: "(min-width: 62rem)";@media #{$bp-wide} {.card { padding: 32px; }}
After a plain $ to @ swap
@bp-wide: "(min-width: 62rem)";@media #{@bp-wide} {.card { padding: 32px; }}
LESS the prelude actually accepts
@bp-wide: "(min-width: 62rem)";@media @bp-wide {.card { padding: 32px; }}

The broken middle form is easy to miss because the braces still match. LESS does not interpolate a Sass-style #{@name} in a media prelude. The desk rewrites @media #{$bp-wide} to @media @bp-wide and records the line under Renamed, because the two spellings compile to different CSS if you get them wrong.

Mixins lose a keyword, keep the call

Sass split the idea of a class from the idea of a mixin. You write @mixin pill when you want a callable block, .pill { } when you want a selector in the CSS. LESS never split them. A class is already callable.

So the mechanical part is boring, which is why people stop reading here.

@mixin pill($bg, $fg: #fff)
Becomes .pill(@bg; @fg: #fff). Commas inside a LESS signature are list separators, so the real argument breaks turn into semicolons. A parameter that held a comma-separated list needs a second look.
@include pill($brand)
Becomes .pill(@brand);. Parentheses make the call unambiguous. Turn the Rewrite @include toggle off if you would rather walk the call sites yourself.
@include truncate;
Becomes .truncate();. Sass had already decided this name is a mixin. LESS will also emit a .truncate selector if a block with that name exists as a class, so a name used both ways in the old Sass needs a decision you, not a converter, should make.
@extend .card;
Moves onto the selector as &:extend(.card). Close enough for most files. Selector order still drifts around media queries, so compile the LESS and compile the original SCSS, then diff the CSS.
@include stack { ... }
This is @content. LESS has no slot that receives a block from the call site. The desk leaves the construct in place under No spelling. Turn the inner block into its own mixin, or into a detached ruleset, by hand.

Control flow LESS never grew

Sass put loops in the language. LESS put each() on a list and stopped there. The difference matters the moment a theme file walks a map of colour names.

@each $name, $color in $tones
No LESS directive does this. Expand the loop once in Sass, or write a LESS mixin per value. The leftover sample on the desk exists so you see a file the pass must refuse.
@for $i from 1 through 12
Same refusal. A column grid you generated with Sass has to be written out, or rebuilt with a LESS mixin that takes an index you pass yourself.
@if $cond { } @else { }
LESS guards hang off a mixin signature with when. They do not sit in the middle of a ruleset. An @if inside .card has nowhere to go. Move the branch into a mixin, or pick one branch and delete the other.
@function tone($key) { @return ... }
LESS has no user function. Colour helpers on the LESS side are mixins or built-ins. A helper that returns a value has to become a variable, a mixin that sets a property, or a small piece of JavaScript in a LESS plugin, which this page will not write for you.

If the leftover score climbs past a handful of lines, stop converting. Run the file through the SCSS compiler and ship CSS. Forcing LESS onto a Dart Sass module is how teams spend a week renaming maps into mixins they will never touch again.

Three Sass ideas with no LESS noun

I keep these together because they arrive in a cluster in any file written after 2019.

Maps.$tones: (ink: #1a2420, leaf: #2a6b4a) is ordinary Sass. LESS stores one value per name. Flattening a map means inventing a naming scheme (@tone-ink, @tone-leaf) and rewriting every map.get. The desk flags the declaration and every lookup. I will not auto-flatten, because the names you pick become your public API.

Placeholders.%card-base emits nothing until something @extends it. LESS has no silent selector. The usual repair is a mixin you call, which copies declarations instead of merging selectors, so the compiled CSS grows. Accept the duplication or keep the file in Sass.

Modules.@use "tokens" as t loads a file once and puts $brand at t.$brand. LESS @import pastes names into one pool. The desk turns a load line into @import and strips the namespace, which is a behaviour change even when the path is the same. Private names starting with an underscore stay public in LESS. A Sass file that leaned on that privacy will leak helpers after the pass.

Built-in modules are quieter. @use "sass:math" exists so math.div($gutter, 2) divides. LESS still reads a slash as division, so the desk writes @gutter / 2 and drops the load line. @use "sass:map" has no such courtesy. If map calls remain, the load line leaving does not fix them.

!default is the one gift going this way

Sass assigns on first write. $brand: #2a6b4a !default; keeps an earlier value if one exists, which is how a theme file overrides a library token by declaring the name first.

LESS assigns lazily. The last declaration in a scope wins everywhere in that scope, including on lines above itself. Dropping !default is often correct because LESS never needed the flag. The desk strips it when the toggle is on.

The gift has a cost. A Sass file that set $gutter: 24px, used it, then set $gutter: 32px later compiled to 24px at the use site. The same two lines in LESS compile to 32px. Search the source for a name declared twice before you trust a clean leftover score. The desk does not simulate either evaluator, so a redeclared token is a Read twice entry, not a silent fix.

Who still asks for LESS

Most migrations run the other way. The requests I still see for this direction are narrow.

If the file never had Sass-only syntax, you might not need a converter at all. Nested CSS with custom properties is valid in current browsers. The CSS to LESS page nests flat CSS when the source never used a preprocessor. Going SCSS to LESS to CSS is a round trip you should skip.

A pass you should not trust blindly

After a clean rename, run the output through a LESS beautifier only if the indent drifted. Do not beautify leftover Sass. You would hide the lines the desk asked you to rewrite.

Questions about handing SCSS to a LESS pipeline

Custom properties, dropped modules, loops, argument separators, and the files you should compile to CSS instead.

Why did --brand stay while $brand became @brand?

They are different languages sharing a page. $brand is a Sass variable erased at compile time. --brand is a CSS custom property the browser keeps, which is why runtime theming uses it. A converter that rewrites dashes will smash a theme file that already bridges the two, which a lot of current SCSS does via :root { --brand: $brand; }. The correct LESS is :root { --brand: @brand; }. The custom property name does not move.

My @use "sass:math" line disappeared. Is the file broken?

Only if math.div calls remain. Dart Sass moved division off the slash operator, so SCSS writes math.div($gutter, 2) and loads the math module. LESS still divides with a slash, so the desk writes @gutter / 2 and drops a load line that would be a syntax error in lessc. @use "sass:map" is the opposite case. Dropping the load line does not create a LESS map, and every map.get stays flagged.

Why is @each still in the LESS pane?

Because LESS has no each-over-a-map directive. each() on a list exists, and it does not walk a Sass map of names to colours. Expanding the loop in Sass, or writing one LESS mixin per value, is the repair. The desk leaves @each in the output so you do not ship a guessed unrolling that misses a pair. Load the Module leftovers sample to see a file this pass is supposed to refuse.

The mixin signature now uses semicolons between arguments. Did something break?

LESS reads commas inside a signature as list separators, which is why semicolons mark the real argument breaks. Sass reads commas as the breaks. .pill(@bg; @fg: #fff) is the LESS form of @mixin pill($bg, $fg: #fff). Check any parameter that used to hold a comma-separated list, because that list now needs parentheses so LESS keeps it as one value.

Does !default mean anything after conversion?

LESS has no such flag. Variables resolve lazily, so the last declaration in a scope wins everywhere in that scope, including above itself. Dropping !default is usually right. The trap is a name declared twice in the Sass with different values, because Sass used the value standing at the moment of use and LESS will use the later one. Search for repeats even when the leftover score is zero.

What do I do with a %placeholder?

LESS has no silent selector. %card-base emits nothing in Sass until something @extends it. The usual LESS repair is a mixin you call, which copies declarations instead of merging selectors, so the compiled CSS gets larger. If selector merge was the point, keep that file in Sass or compile it to CSS and stop pretending LESS is the same language with a different sigil.

Will SCSS to LESS to SCSS round-trip?

On a token sheet of variables, nesting, and mixins, close enough that a diff stays readable. On a Dart Sass module with maps, @use namespaces, and @each, no. The leftover lines never became LESS, so feeding them to the LESS to SCSS converter produces Sass that still contains Sass, plus a second round of flags. Convert only the files whose leftover score is empty, and rewrite the rest by hand or ship CSS.

Should I flatten maps or compile to CSS instead?

Compile to CSS if nobody has to edit the LESS. Flatten by hand if a vendor file must import named tokens. Flattening invents a public naming scheme (@tone-ink, @tone-leaf) you will live with, which is why this desk will not invent those names for you. A two-pipeline shop is often better served by CSS custom properties both compilers emit, rather than by keeping a LESS twin in sync.

Does my stylesheet leave the browser?

No. The rewrite and the shrinkage report run in JavaScript inside this page. After load, nothing is posted, nothing is stored between visits, and closing the tab clears both panes. A client palette under embargo stays on your machine.

Why convert to LESS at all now?

Because a vendor skin, an email pipeline, or a client repo never left it. New work should not pick LESS to be fashionable. If you control both ends, compile SCSS to CSS or move the shared tokens into custom properties. Use this page when the LESS side is a constraint you did not choose, and when the SCSS you are handing over is still mostly variables, mixins, and nesting.