Whitespace is the syntax, so it gets read first
SCSS and LESS both use braces, which is why an SCSS to LESS pass is mostly a rename. Indented Sass is different. Nothing in a .sass file says where a block ends except the column the next line starts in. Stage one walks every line, measures its leading whitespace against the first indented line in the file, and turns each drop in depth into a closing brace. A line with a more indented line under it opens a block. A line without one is a declaration.
Two consequences follow. A file mixing tabs and spaces has no single unit to measure against, so the page converts tabs to the space width it found and flags the file rather than guessing silently. Load the fourth sample to see what that looks like. And the colon rule from the Sass spec decides what counts as a property: color: red with a space after the colon is a declaration, a:hover without one is a selector. A line like color:red is read as a selector, exactly as Dart Sass reads it, and the flagged list says so instead of quietly turning a pseudo class into a property.
What each Sass spelling becomes
Stage three is a lookup, and the whole lookup fits in one table. The left column is the indented syntax as people write it. The middle is what a brace based SCSS file would say. The right is what lands in the LESS pane.
| Indented Sass | Braced step | LESS |
|---|---|---|
$gap: 16px | $gap: 16px; | @gap: 16px; |
$gap: 16px !default | $gap: 16px !default; | @gap: 16px; with a note |
=shadow($y) | @mixin shadow($y) { | .shadow(@y) { |
+shadow(4px) | @include shadow(4px); | .shadow(4px); |
@extend .btn | @extend .btn; | &:extend(.btn all); |
%reset block | %reset { | .reset() { and each @extend %reset becomes .reset(); |
.col-#{$n} | .col-#{$n} { | .col-@{n} { |
math.div($w, 3) | math.div($w, 3) | (@w / 3) |
@import base | @import "base"; | @import "base"; |
font: with indented family: | font: { family: ... } | font-family: ...; |
@each, @if, @for, @function | same, with braces | kept as written and flagged |
@use, @forward, @content | same | kept as written and flagged |
The = and + shorthand only exists in the indented syntax, and it is the reason a generic Sass to LESS regex fails on real .sass files. A rule starting with + is a mixin call, not an adjacent sibling combinator. The parser checks for a mixin name directly after the sign before deciding, so + .sibling with a space still reads as a selector.
One component through all three stages
A card partial written the way indented Sass encourages: nested properties under font:, a mixin defined with =, and a hover state on the parent.
$ink: #1d365d
=lift($y: 4px)box-shadow: 0 $y 18px rgba(29, 54, 93, .14).tx-card
+lift
color: $ink
font:family: Inter, sans-serif
size: 15px
&:hover
+lift(10px)a:hover
text-decoration: underline@ink: #1d365d;.lift(@y: 4px) {box-shadow: 0 @y 18px rgba(29, 54, 93, .14);}
.tx-card {.lift();color: @ink;font-family: Inter, sans-serif;font-size: 15px;&:hover {.lift(10px);}
a:hover {text-decoration: underline;}}Three details are worth checking against your own file. The nested font: block flattened into two hyphenated properties, because LESS has no nested property syntax and would read font: { as a detached ruleset. The bare +lift call became .lift() with empty parentheses, which is how LESS calls a mixin with default arguments. And a:hover stayed a selector while color: $ink became a declaration, which is the colon rule from the first section doing its job.
Why some lines stay put
LESS stopped adding language features around the time Sass added modules, so a share of any modern .sass file has no LESS equivalent. Rather than drop those lines or invent a translation, the converter leaves them where they are and lists them under the output with a reason. Each entry falls into one of two groups.
- Notes. The line converted, and you should know what changed.
!defaultwas removed because LESS uses lazy evaluation and last-wins for the same effect. A%placeholderblock became a parametric mixin, which compiles to nothing on its own and outputs only where called, the closest LESS gets to the same idea. Turn that off with the checkbox if you would rather see the placeholder stay and fix it by hand. - Stops. The line is still Sass.
@eachover a map,@ifand@else,@functionwith@return,@usewith a namespace,@contentinside a mixin, andmap-get()all land here. LESS has loops built from recursive mixins and guards, but rewriting a Sass loop into that shape changes the structure of the file, and a tool doing it for you produces LESS nobody on the team recognises.
Load the third sample to see a partial that is half stops. A file like that is a signal to change approach: compile the Sass to plain CSS with the Sass to CSS compiler, then bring the flat CSS across with CSS to LESS. You lose the loop, but you keep every rule the loop generated, and the LESS you get is honest about what it is.
Checking the result before it ships
The output is a new file, not a formatted copy, so treat the first conversion of a real partial as a diff to read. The cheapest check is to compile both sides. Run the original through Sass to CSS, run the output through LESS to CSS, and compare the two stylesheets. When they match rule for rule, the conversion is done. When they differ, the difference is almost always one of these:
- Division.
math.divdivides unconditionally. LESS in strict math mode only divides inside parentheses, which is whymath.div($w, 3)comes out wrapped as(@w / 3). A plain$w / 3from older Sass is left as@w / 3, which LESS 4 prints as written instead of dividing. Wrap those by hand. - Extend scope. Sass
@extend .btnalso extends.btn:hoverand.btn.is-active. LESS needs theallkeyword for that, which the converter adds. Remove the keyword when you only meant the bare selector. - Colour functions.
darken,lighten,mix, andrgbaexist in both. Sasscolor.adjustandcolor.scaledo not, and anycolor.prefix is flagged as a stop.
Where this converter stops
Everything runs in your browser tab. Nothing is uploaded, nothing is kept between visits, and a 2,000 line partial converts in under 50 milliseconds. The limits are in what the parser understands, not in size. Multi-line property values, where a long grid-template-areas value continues onto the next line, are read as a new block and produce a stray brace. Old-style :color red declarations with the colon in front are handled, but a +mixin call inside an interpolated selector is not. And the indented syntax is rare enough in 2026 that most projects reaching this page are converting a legacy file once. For an active codebase with a build step, running sass-migrator to move to SCSS first, then using SCSS to LESS, gives you a reviewable diff at each step instead of one large jump.
