SASS to LESS Converter

Indented .sass has no braces and no semicolons, so whitespace is the only thing holding the structure. This page reads that whitespace back into blocks, then swaps the Sass dialect for LESS one construct at a time. Anything LESS has no spelling for stays in place and shows up in the flagged list with its line number.

SASS to LESS conversion pipeline

  1. 1Read indentationwaiting
  2. 2Rebuild braces0 blocks
  3. 3Swap dialect0 rewrites
.sass
LESS.less

LESS appears here as you type.

Samples

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 SassBraced stepLESS
$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, @functionsame, with braceskept as written and flagged
@use, @forward, @contentsamekept 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.

_card.sass14 lines
$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
card.less0 stops
@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.

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:

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.

Questions that come up on the first real file

Answered from the cases this parser sees most.

Why did the converter flag my whole file for indentation?

One line uses a tab where the rest use spaces, or the other way round. The parser measures every line against the first indented line it sees, so a single stray tab shifts the block depth for everything below it. The flag names the line. Fix that line in the input and the rest of the flags for that file usually disappear.

My a:hover rule turned into a block but color:red did not. Why?

Dart Sass decides property versus selector by the space after the colon. color: red with a space is a declaration. color:red without one is a selector, so it opens a block, and the flagged list points at it. Add the space in the source file and convert again.

What happened to +clearfix with no parentheses?

It became .clearfix(); with empty parentheses. LESS calls a mixin with default arguments that way, and the parentheses also stop LESS from copying a plain .clearfix rule into the output as a class. If the mixin was defined with =clearfix and no arguments, the definition became .clearfix() { too, so the pair matches.

Does the converter handle the sass:math module?

The math.div() calls inside the file are rewritten to parenthesised division, which is the LESS spelling. The line loading the module has no LESS equivalent and stays in the output as a stop, so delete it once the divisions look right. Other namespaces such as color. or map. are flagged rather than rewritten.

Why is !default gone from my token sheet?

LESS resolves variables lazily and the last definition in scope wins, which covers the main use of !default: letting a later file override a library default. Removing the keyword is the correct translation and each removal is listed as a note so you know it happened.

Can I get the braced SCSS instead of LESS?

Tick Show the braced step and the middle pane displays stage two, which is valid SCSS with braces and semicolons and no dialect changes. Copy from that pane if SCSS is the real target, or use the SASS to SCSS page which is built around that single conversion.

Will the LESS compile back to the same CSS as my Sass?

When the flagged list is empty, yes, with one caveat on division in LESS strict math mode covered above. When the list has stops, no, because those lines are still Sass. Compile both sides with the Sass to CSS and LESS to CSS pages and diff the results rather than trusting the conversion on sight.