Stylus to LESS Converter

Stylus drops braces, semicolons, colons, and the variable sigil. LESS wants all four back. Paste your .styl file, watch the structure get rebuilt from the indentation, then read the line ledger under the panes for the constructions LESS never grew a word for.

Stylus to LESS conversion workspace

Sample
Indent
Stylus, indentation and allRewrites as you type
LESS with the punctuation backRead only

Line ledger

Paste Stylus above. Every line the pass touched is filed below with the reason.

0 mapped0 read twice0 no LESS form

Stylus took the punctuation out. LESS wants it back.

Going from SCSS to LESS is a vocabulary problem. Both languages already agree on shape, so the work is finding the Sass nouns LESS never grew. Stylus is a different job. A .styl file has no braces around a block, no semicolon after a declaration, often no colon between a property and its value, and no sigil in front of a variable name. Four kinds of punctuation are missing, and LESS needs all four. The structure has to be rebuilt from the indentation before a single name gets translated.

The workspace above reads your paste twice. The first pass walks every line looking for two things: names assigned with =, and blocks whose header looks like name(args) with something indented under it. That gives a list of your variables and a list of your mixins. The second pass walks the indentation column by column, opens a brace when a line has children, closes one when the column drops, and uses the two lists from the first pass to decide whether a bare word is a variable, a mixin call, or a CSS keyword.

A converter with no first pass has to guess, and the guesses are wrong in the same places every time.

Read the ledger under the panes before you run lessc. This page evaluates nothing, follows no import path, and never invents a LESS form for a Stylus hash. A line with no LESS spelling stays written as Stylus in the output on purpose. A guess there would hide the work you still owe the file.

The four things Stylus leaves out

  1. Braces Block boundaries come from the indentation column. A tab counts as four columns here. If your editor wrote a mix of tabs and spaces, the tree comes out at the wrong depth and the braces land in the wrong place.
  2. Semicolons Appended to every declaration, every variable assignment, and every mixin call. LESS treats a missing semicolon as a parse error, not a warning.
  3. Colonspadding 20px means padding: 20px. The pass adds the colon when the first word is a CSS property it knows, and flags the line when it is not.
  4. Sigilsgutter = 24px declares a variable and padding gutter uses one. Neither line carries a marker. This is why the symbol table exists, and why converting one file at a time has a hard limit.

The @ trap, and why it is filed as a refusal

Stylus and LESS both use @. They do not mean the same thing by it, and the characters survive a naive conversion unchanged, so nothing errors and the CSS quietly comes out wrong.

In Stylus, @width inside a block is a property lookup. It reads the value of the width property already declared in that same block. In LESS, @width is a variable reference.

Stylus, compiles to -160px
.card
width 320px
margin-left (@width / -2)
Characters kept, meaning gone
.card {width: 320px;margin-left: (@width / -2);}

If no @width variable exists, lessc errors and you find the line. If one exists in another file, and in a large LESS tree one usually does, the property takes that value instead and nothing complains. That second case is why the ledger files this under No LESS form rather than rewriting it.

The repair is to give the value a name of its own.

@card-width: 320px;.card {width: @card-width;margin-left: (@card-width / -2);}

One line, two readings

Stylus lets a mixin be called with no parentheses, so margin auto is either the margin property set to auto, or a call to a mixin named margin with the argument auto. The Stylus compiler settles it by knowing which mixins exist. So does this page, inside the boundary of one paste.

Line in the sourceWhat the pass writesWhy
border-radius 4pxborder-radius: 4px;The first word is a CSS property, so the line is a declaration with the colon put back.
truncate().truncate();Parentheses, and a block headed truncate() sits earlier in the paste.
clearfix().clearfix();Nothing defines it here. Written as a call because the toggle is on, filed under Read twice.
pill(brand).pill(@brand);Defined above, and brand is in the symbol table so it picks up a sigil.
glow 4pxglow: 4px;Not a property this page knows and not defined as a mixin. Written as a declaration, filed under Read twice.

Switch the mixin toggle off when you paste a partial file. Unknown bare calls then stay untouched in the output instead of turning into .name() lines you have to undo. Convert the file holding your mixin bodies first, and the ambiguity disappears on its own.

Commas break in different places

LESS reads a comma inside a mixin signature as a list separator, so the real argument breaks are semicolons. Stylus reads commas as the argument breaks. Both directions of that difference show up in the output.

pill(bg, fg = #fff)
Becomes .pill(@bg; @fg: #fff). The default value moves from = to : and the separator becomes a semicolon.
pill(#eef1f6, #1d365d)
Becomes .pill(#eef1f6; #1d365d). Left as a comma, LESS would hand the mixin one list argument and fail on the missing second parameter.
font-stack(Inter, system-ui, sans-serif)
Three arguments after conversion, which is almost certainly not what you meant. Wrap a comma separated value in parentheses in the Stylus first, so it survives as one item.
@extend .tag
Moves onto the selector as &:extend(.tag). Close enough for a flat sheet. Stylus and LESS merge selector lists at different points, so compile both trees and diff the CSS when media queries are involved.

Four constructions LESS never grew

These arrive together in any Stylus file written past the token sheet stage, and none of them has a mechanical translation.

tones = { ink: #1c2430, leaf: #2a6b4a }
A hash. LESS holds one value per name. Flattening means inventing a naming scheme like @tone-ink and rewriting every tones.ink and tones[key] lookup. The names you pick become what other files import, which is why this page will not pick them for you.
for s, i in sizes
LESS spells a loop as a mixin that calls itself with a guard on the index. It is not a directive you drop inside a ruleset. Unroll the loop in Stylus first, or write the recursive mixin by hand.
if hairline / else
LESS guards hang off a mixin signature with when. A branch sitting in the middle of a block has nowhere to go. Move the branch into a mixin, or pick the one branch you ship and delete the other.
tone(key) with a return
LESS has no user function. A helper that hands back a value has to become a variable you declare, a mixin that sets the property directly, or a LESS plugin written in JavaScript, which is a bigger commitment than the file you were porting.

Watch the refusal count as you paste. Past a handful of lines, converting stops being the cheap option. Two better exits exist. Compile the file with the Stylus to CSS page and ship plain CSS when nobody needs to edit the preprocessor source again. Or go to Stylus to SCSS instead, because Sass has maps, loops, conditionals, and functions, so the constructions LESS refuses land almost one for one.

When the indentation is lying to you

What this page will not do for you

Once the refusal count reads zero, run the output through the LESS compiler and fix whatever the first error points at. Reach for the LESS beautifier only after that. Beautifying a file that still holds refused lines reindents the exact lines the ledger asked you to rewrite, and they stop looking out of place.

Questions about moving a .styl file onto lessc

Sigils, property lookups, ambiguous bare calls, argument separators, and the files you should compile instead.

Why did my variable names pick up an @ but the loop counter did not?

The pass builds a symbol table from lines assigned with =, then adds the sigil to every use of those names. A loop counter is introduced by the for header itself, not by an assignment, so it never enters the table. The loop is filed as a refusal anyway, because LESS spells iteration as a recursive mixin with a guard rather than a directive you drop inside a ruleset. Rewrite the loop and the counter becomes a real mixin parameter.

What is wrong with my @width line? It looks like valid LESS.

It is valid LESS. It means something else. In Stylus @width inside a block is a property lookup that reads the width already declared in that same block. In LESS it is a variable reference. The characters survive conversion unchanged, so no error appears. If a variable named width exists anywhere in your LESS tree, the property silently takes that value. Declare a real variable, use it for both the width and the calculation, and the ambiguity is gone.

Why is margin auto sometimes a declaration and sometimes a mixin call?

Because Stylus allows a mixin to be called without parentheses, so the line is genuinely ambiguous without knowing which mixins exist. Stylus resolves it against everything it loaded. This page resolves it against one paste, using the mixin bodies it found on the first read. Anything it cannot place lands under Read twice in the ledger. Turn the bare call toggle off when you paste a partial file, and unknown lines stay untouched.

My mixin signature uses semicolons now. Did the converter break it?

No. LESS reads a comma inside a signature as a list separator, so semicolons are what mark the real argument breaks. .pill(@bg; @fg: #fff) is the LESS spelling of pill(bg, fg = #fff). The parameter to watch is any one that held a comma separated value, such as a font stack or a shadow list. Wrap that value in parentheses so LESS keeps it as a single item instead of splitting it into extra arguments.

What replaces a Stylus hash in LESS?

Separate variables, and a rewrite of every lookup. LESS stores one value per name, so tones = { ink: ..., leaf: ... } becomes @tone-ink and @tone-leaf, and every tones.ink or tones[key] has to be replaced by hand. The page flags the hash and each lookup instead of flattening it, because the names you choose turn into the interface other files import. A generated naming scheme would lock you into a decision you never made.

Can I keep my for loops after converting?

Not as loops. LESS iterates with a mixin that calls itself and stops on a guard, which is a different shape from for s, i in sizes. Two repairs work. Unroll the loop in Stylus before you paste, so the output holds the generated rules directly. Or write the recursive LESS mixin once and call it with the list. Loading the Refusals sample shows a file this pass is meant to refuse rather than approximate.

My Stylus file already uses braces and semicolons. Does that still convert?

Yes, as long as each rule still spans several lines. A trailing opening brace is absorbed rather than doubled, and a closing brace sitting on its own line is dropped because the indentation walk already closed that block. Both show up in the ledger under Read twice so nothing goes missing quietly. The case that breaks is a whole rule written on one line, because the walk has no column change to read. A file that already uses braces and semicolons throughout is closer to LESS than to idiomatic Stylus anyway, so the work left is mostly sigils and mixin signatures.

Should I be moving to LESS or to SCSS?

SCSS, unless the LESS side is a constraint you did not choose. Sass has maps, loops, conditionals, and functions, so a Stylus file converts with almost nothing refused. LESS dropped its own growth around the time Stylus was at its most expressive, which is why hashes, branches, and return values have nowhere to land. Pick this direction when a vendor theme, an email pipeline, or a client repo already runs lessc and is not moving.

Does my stylesheet leave the browser?

No. The symbol table, the indentation walk, and the ledger all run in JavaScript inside this page. After the page loads nothing is posted anywhere, nothing is stored between visits, and closing the tab clears both panes. An unreleased brand palette stays on your machine.

Will Stylus to LESS and back again give me my file?

On a token sheet of variables, nesting, and simple mixins, the round trip stays readable in a diff. On a real Stylus file it does not. Property lookups, hashes, loops, and functions never became LESS, so handing the output to a LESS to Stylus pass gives you Stylus that still contains Stylus, plus a second round of flags. Convert only the files whose refusal count is zero, and rewrite the rest deliberately.