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
- 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.
- Semicolons Appended to every declaration, every variable assignment, and every mixin call. LESS treats a missing semicolon as a parse error, not a warning.
- Colons
padding 20pxmeanspadding: 20px. The pass adds the colon when the first word is a CSS property it knows, and flags the line when it is not. - Sigils
gutter = 24pxdeclares a variable andpadding gutteruses 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.
.card
width 320px
margin-left (@width / -2).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 source | What the pass writes | Why |
|---|---|---|
| border-radius 4px | border-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 4px | glow: 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-inkand rewriting everytones.inkandtones[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
- Mixed tabs and spaces. A tab counts as four columns here. Editors that render it as two or eight will show you a tree the pass does not see. Normalise the whitespace before you paste, and the brace depth stops surprising you.
- Hybrid files. Stylus accepts braces and semicolons too, so plenty of real files carry both styles in different places. A trailing
{is absorbed instead of doubled, and a closing brace on its own line is dropped because the depth walk already closed that block. What does not survive is a whole rule written on one line, since the walk has no column change to read. - Selector lists split over lines. A line ending in a comma is carried through without opening a block, so
.a,above.bcomes out as one selector list. A long declaration value wrapped across lines does not survive the same way. - Outdented comments. Comment lines are skipped while the pass measures the next child depth, so a comment flush against the left margin inside a block will not close that block early.
What this page will not do for you
- One file per paste. Import paths are never followed. A mixin living in another .styl file looks undefined here, which is exactly why the bare call toggle exists. Convert the mixin file first, then its callers.
- No arithmetic, no colour math.
lighten(brand, 10%)becomeslighten(@brand, 10%). LESS carries the same name, so the call usually survives, and the two compilers still disagree at the edges of the colour space. Compile both trees and diff the CSS before you trust a palette. - Nothing gets guessed. Property lookups, hashes, loops, branches, and functions are copied through as Stylus so the gap is visible in the output rather than buried in a report.
- Unit handling is loose in Stylus. Stylus will happily add a unitless number to a pixel value. LESS is stricter about some of the same expressions, so an arithmetic line that never complained before is worth compiling early.
- Your stylesheet stays in this tab. The parse, the rewrite, and the ledger all run in JavaScript on the page. Nothing is uploaded, nothing is kept between visits, and closing the tab clears both panes.
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.
