One LESS file, three Stylus dialects
Most preprocessor converters on this site have one output. Going LESS to SCSS means swapping a sigil and renaming a few functions, and the shape of the file never moves. Stylus is different because Stylus itself never settled on a shape. The compiler accepts color brand, color: brand, and color: brand; inside braces, and treats all three as the same declaration. A converter with one fixed output picks a dialect for you. This one puts the choice on a dial above the panes, because the answer depends on who reads the file next, not on what the compiler accepts.
Why the dial comes first
The three settings produce the same CSS. They differ in what a reviewer sees in the pull request and in how the file reads six months later.
- Bare drops braces, colons, and semicolons. Nesting is carried by indentation alone. This is how the Stylus documentation writes examples and how most
.stylfiles in the wild look. Pick this when the file is joining a codebase already written in Stylus. - Colons kept keeps
property: valuebut still drops braces and semicolons. A value beginning with a word, such asfont-family Interortransition opacity .2s, reads as two words without a colon. The colon restores the boundary. Pick this when the team is new to Stylus and will be reading more than writing. - Braces kept keeps everything: braces, colons, semicolons. Stylus calls this its CSS-style syntax. The diff against the LESS source is then nothing but the sigils and mixin heads, which makes the conversion commit reviewable line by line. Pick this for the first commit, then reformat later if the team wants the bare form.
Variable assignments come out as name = value on every setting, since Stylus has no other spelling for them. Mixin definitions, @extend lines, and mixin calls follow the same rule and only pick up a semicolon under the braces setting.
The at-sign was doing three jobs
In LESS the @ character starts a variable, starts an at-rule, and opens an interpolation. Stylus keeps the character for at-rules only. The converter sorts each use by position:
// LESS
@brand: #0f5c5a;@wide: ~"(min-width: 62rem)";.col-@{n} { width: ~"calc(100% / @{n})"; }
@media @wide { .masthead { padding: (@gutter * 2); } }
brand = #0f5c5a
wide = unquote("(min-width: 62rem)").col-{n}
width unquote("calc(100% / " + n + ")")@media wide
.masthead
padding (gutter * 2)A leading @name: is an assignment. A leading @media, @supports, @font-face, @keyframes, or any other name on the CSS at-rule list keeps its sigil and has its query rewritten. An at-rule name the page does not know is copied through and marked, because a LESS file with @plugin or a custom at-rule needs a human to say what it meant. Everything else with a sigil is a variable reference and loses it.
One consequence deserves a warning of its own. Stylus decides whether a bare word is a variable or a CSS keyword by checking whether the word was ever assigned. color red is the keyword red when nothing named red exists, and your variable when something does. A LESS file with @red, @auto, @none, or @large converts without error and compiles without error, and every place the keyword was meant as a keyword now reads the variable instead. The page marks each assignment whose name is also a CSS keyword. Rename those before you convert the rest of the tree.
A LESS class is a mixin. A Stylus class is a class.
LESS lets any ruleset be called as a mixin. Write .btn { padding: 8px; } and later .btn; inside another block, and the padding is copied in. Stylus separates the two. A mixin is defined without a dot and called without a dot. A class is only ever a selector.
// LESS
.pill(@bg; @fg: #fff) { background: @bg; color: @fg; }
.truncate() { overflow: hidden; white-space: nowrap; }
.tag { .pill(@brand); .truncate(); }
pill(bg, fg = #fff)background: bg
color: fg
truncate()overflow: hidden
white-space: nowrap
.tag
pill(brand)truncate()Parameter lists come across with a comma between parameters, whichever separator the LESS used. Defaults move from @fg: #fff to fg = #fff. A rest parameter @rest... becomes rest..., and @arguments becomes arguments, both of which Stylus supports by those names.
The call that needs attention is the one with no parentheses. .btn; is a legal LESS call to a plain class. The page rewrites it as btn(), then checks whether a .btn ruleset appeared earlier in the paste and no btn() mixin did. When that is the case the line is filed under "to read again", because Stylus will report an undefined mixin. Two repairs work. Replace the call with @extend .btn if you want the selector merged, or turn the original ruleset into a real mixin and call it from the class as well.
Guards fold inside the body
LESS attaches a condition to the mixin head with when. Stylus has no head condition and instead uses if as the first line of the body. The converter moves the condition inside and reindents the body under it:
// LESS
.stripe(@c) when (lightness(@c) > 50%) and (iscolor(@c)) {border-top: 2px solid darken(@c, 30%);}
stripe(c)if lightness(c) > 50% and c is a 'rgba'
border-top 2px solid darken(c, 30%)Three spellings change along the way. A comma between guard clauses means or in LESS and is written out as or. A single = is equality in a LESS guard and becomes ==. The type guards iscolor, isnumber, isstring, and iskeyword become the Stylus is a operator with the matching type name, and the page marks those lines because Stylus has one type for all colours and one for all numbers with units, which is coarser than what LESS was checking.
Two guard patterns have no Stylus form. A mixin defined twice with different guards, which LESS resolves by trying each in turn, has to become one Stylus function with an if and an else. The page converts each definition and marks the second one so you know to merge them. The default() guard, which matches when no other definition did, is left as LESS. So is a guard on a plain selector, such as button when (@mode = dark), since Stylus applies conditions to declarations and blocks rather than to selectors.
extend, escape, and interpolation
These three are where LESS and Stylus agree on the idea and disagree on the spelling.
- Extend.
&:extend(.tag);becomes@extend .tag. The same rewrite happens when the extend hangs off the selector, as in.tag-warn:extend(.tag) {, with the@extendline placed first inside the block. The LESSallkeyword, which also extends.tag:hoverand every other compound selector containing.tag, has no switch in Stylus. The page drops the keyword and marks the line. Compile and check whether the hover and focus variants were part of what you wanted copied. - Escape.
~"(min-width: 62rem)"ande("calc(100% - 10px)")both becomeunquote("..."). The%("%d of %d", @a, @b)format function becomes the Stylus%operator:"%s of %s" % (a b). - Interpolation. Outside a string,
@{n}becomes{n}and works in selectors, property names, and at-rule queries. Inside a string Stylus does not interpolate at all, so"@{host}/mark.svg"becomes the concatenationhost + "/mark.svg". Concatenation in Stylus produces a quoted string, which is what aurl()or acontentvalue wants. Both are marked as worth a second read because a string that was built to be unquoted, such as a media query, needs theunquote()wrapper the tilde form gets automatically.
A handful of functions are renamed rather than flagged. fade(@c, 12%) becomes rgba(c, 0.12), fadeout and fadein become fade-out and fade-in, greyscale becomes grayscale, and the LESS if(cond, a, b) function becomes the Stylus ternary (cond ? a : b). Colour functions with the same name on both sides, such as lighten, darken, mix, and spin, are carried by name and marked, because the two compilers round differently at the edges of the colour space and a palette is worth diffing after the first compile.
What stays as LESS
Every line the page cannot translate is copied through unchanged, given a red mark in the output gutter, and listed under "Left as LESS" with its source line number. Clicking the number moves the cursor to the line in the Stylus pane. The list is deliberate. Guessing at these would produce Stylus that compiles and does the wrong thing, which is worse than Stylus that refuses to compile until you look.
- Detached rulesets.
@shared: { ... }and the call@shared();. Stylus has no block-valued variable. Rewrite as a mixin with no parameters. - Namespaces.
#ns > .reset();and#ns.reset();. Stylus mixins live in one flat scope. Flatten the names, or move the namespace into a separate file and@requireit. - Variable variables.
@@theme-inkreads the variable whose name is stored in another variable. Stylus has no equivalent lookup. A hash,tones = { ink: ..., leaf: ... }, indexed withtones[theme], is the usual replacement. - Pattern-matched mixins.
.size(large) { ... }matches on a literal argument. Stylus wants onesize(kind)with anif kind == largeinside. - Property merge.
background+: url(...)andbackground+_: no-repeatjoin values across declarations. Stylus has no merge operator. Build the value once. - !important after a call.
.rounded() !important;marks every declaration the mixin emits. Stylus has no call-level flag. Add!importantinside the mixin or on the declarations it lands on. - Backtick JavaScript, @plugin, and functions with no counterpart.
each(),range(),data-uri(),image-size(),svg-gradient(), and theisurlfamily are all left in place.
Two limits sit outside the list. The page reads one paste at a time and never follows an @import, so a mixin defined in another file looks undefined here and a class defined in another file cannot be recognised as a class. Convert the file with the definitions first. And the conversion runs entirely in this tab. The parse, the rewrite, and the receipt are JavaScript on the page. Nothing is uploaded and nothing is kept between visits.
Is Stylus where this file should go?
Two situations make this page the right one. A codebase already written in Stylus is taking on a LESS component, and one dialect in a tree is better than two. Or a build pipeline that runs Stylus, often with Nib or Kouto Swiss beside it, is inheriting a LESS theme and nobody wants a second preprocessor in the toolchain.
If neither is true, look at the destination before converting. Stylus releases have been infrequent for years, several bundler integrations treat the language as a legacy input, and the flat scope for mixins is the reason namespaces and detached rulesets have nowhere to land. A file leaving LESS with no Stylus codebase pulling it is usually better served by LESS to SCSS, where maps, control flow, and modules all have a home. A file nobody will edit again is better served by the LESS compiler, which produces plain CSS and ends the question.
When the destination is right, work down the receipt. Rename any variable the page marked as a keyword clash. Rewrite the lines under "Left as LESS". Compile the result, then compile the original with LESS to CSS and diff the two outputs. Reformat with the bare dialect only after the diff is empty, because reindenting a file that still holds LESS lines makes those lines look like they belong.
