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
@medianesting,@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
% @usenamespaces and@forward- User
@functionwith@return @each,@for,@while@at-root, mixin@contentblocks
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.
$bp-wide: "(min-width: 62rem)";@media #{$bp-wide} {.card { padding: 32px; }}@bp-wide: "(min-width: 62rem)";@media #{@bp-wide} {.card { padding: 32px; }}@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@includetoggle 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.truncateselector 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@ifinside.cardhas 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.
- A vendor skin that never left LESS. Bootstrap 3 era admin themes, old email CSS pipelines, a component library a client refuses to rebuild. You own one SCSS partial the vendor file has to import.
- A shared token file compiled by two pipelines. One app still on
lessc, one on Dart Sass. The honest fix is CSS custom properties both sides read. The short fix is a LESS twin of the SCSS tokens, regenerated when the Sass side changes. - A designer who pastes SCSS from a new kit into an old LESS repo. Variables and nesting survive. Maps and loops do not. Load the leftover sample on the desk before you promise a full port.
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
- One file per paste. Import paths are not followed. A mixin defined in
_mixins.scsslooks undefined here. Convert the mixin file first, then the callers. - Nothing is evaluated.
darken($brand, 12%)becomesdarken(@brand, 12%). Same name, similar arithmetic, still worth compiling both trees.color.adjusthas no automatic LESS spelling on this page becausefade,fadeout, andspindisagree on edge colours. - Indented Sass is the wrong input. Braces and semicolons have to be present. Convert SASS to SCSS first, or use the SASS to LESS page if the source already dropped braces.
- Selector lists after extend are not compared. Matching CSS after
lesscandsassis the only proof. Source review misses selector order. - Your stylesheet stays in this tab. Parsing and rewriting run in JavaScript on the page. Close the tab, both panes die. An unreleased palette does not hit a server.
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.
