A CSS formatter breaks LESS. That is why this page exists.
Point a plain CSS beautifier at a LESS file and it will do something reasonable to the parts it recognises, then quietly damage the parts it does not. Semicolons separating mixin arguments get treated as statement ends. A colon inside @media (min-width: 48em) gets read as a property. Escaped strings written as ~"..." get reindented from the inside. The formatter on this page walks the source with the preprocessor syntax in mind, so those constructs survive the round trip.
Formatting is only half of what happens when you press Format. The panel above also reports what the file contains: how many variables it declares, how many mixins it defines against how many it calls, whether guards are in play, and the deepest point the nesting reaches. Those numbers say more about whether a stylesheet is going to be pleasant to maintain than the indentation ever will.
@brand:#1d365d;.rounded(@r:6px){border-radius:@r}
.card{background:#fff;.rounded();.title{color:@brand;&:hover{color:lighten(@brand,15%)}}}@brand: #1d365d;.rounded(@r: 6px) {border-radius: @r;}
.card {background: #fff;.rounded();.title {color: @brand;&:hover {color: lighten(@brand, 15%);}}}The at sign does two jobs, and that is where formatters slip
In CSS, @ starts an at-rule. In LESS it also starts a variable. The two look identical to a naive tokenizer, so a CSS formatter treats @primary: #1d365d; as a malformed at-rule and either indents it wrong or drops the space after the colon. The parser here decides what a colon means by looking ahead: if a semicolon or a closing brace arrives before an opening brace, it is a declaration and gets a space after the colon. If an opening brace arrives first, it is part of a selector and stays tight.
That single rule is what keeps a:hover from becoming a: hover while still turning color:red into color: red. It also handles the awkward middle case correctly, since @detached: { color: red; }; is a variable holding a ruleset and needs the brace left alone.
Nesting depth is the number worth watching
Nesting is the feature people reach for first and regret last. Each level you open concatenates into the selector the compiler emits, and the result stops being something you would ever write by hand. Four levels of source nesting produce a selector nobody can override without a fight.
| Source depth | Compiled selector | Verdict |
|---|---|---|
| 1 | .card | Fine |
| 2 | .card .title | Fine |
| 3 | .card .title .badge | Getting long, still readable |
| 4 | .card .body .list .item | Fragile, tied to markup shape |
| 5 or more | .page .card .body .list .item a | Rewrite it as a flat class |
The depth counter in the panel reports the deepest point in whatever you pasted, not an average. One runaway block in a 900 line file is enough to push it to 6, which is the point of showing the maximum rather than smoothing it out. The & parent selector does not add a level, so &:hover and &.is-active are free.
Mixin definitions, mixin calls, and the parentheses that decide what ships
LESS lets you write a mixin two ways, and the difference has nothing to do with style. A ruleset declared with empty parentheses is callable and produces no CSS of its own. The same ruleset declared without them is callable and compiled into the output, whether or not anything calls it.
.helper() { color: #333; }
.helper { color: #333; }The first line adds nothing to your stylesheet until something calls .helper();. The second ships .helper{color:#333} to every visitor forever. That is how a build ends up with dozens of orphan utility classes nobody wrote on purpose. The report counts definitions and calls separately, and flags any call written as .helper; without the parentheses, since that shorthand is the usual sign the definition is missing them too.
Argument separators are the other trap. A mixin whose arguments contain commas has to use semicolons between them, which is exactly the character a CSS formatter treats as end of statement:
.shadow(0 1px 2px rgba(0,0,0,.2); inset 0 0 0 1px #eee);The tokenizer tracks parenthesis depth, so semicolons and commas inside an argument list never trigger a line break. Only the ones at depth zero do.
Guards read like conditions, but they behave like pattern matching
A guard attaches a when clause to a mixin and decides whether that mixin applies. It looks like an if statement and is closer to overload resolution, since every matching definition runs, not only the first.
.label(@c) when (iscolor(@c)) {color: @c;}
.label(@c) when (default()) {color: #333;}Both definitions carry the same name and arity. LESS evaluates each guard and applies all of the ones that pass, with default() acting as the fallback that only fires when no other guard matched. Miss that and you get two colors in the compiled rule, with the last one winning silently. The formatter keeps the guard on the same line as the selector, since splitting it makes the pair harder to compare at a glance.
Three things the formatter refuses to touch
- Escaped strings. Anything wrapped as
~"..."is passed to the output character for character. That is the whole purpose of the construct, since it exists to smuggle syntax the LESS parser would otherwise choke on, such as old IE filter values or acalc()expression with a division in it. Reindenting the inside would change what compiles. - Interpolation. A property name or selector fragment written as
@{name}is resolved at compile time and could be anything. The formatter treats the whole token as one opaque unit rather than trying to reason about what falls out of it. - Backtick expressions. Older LESS allowed inline JavaScript between backticks. Less 4 turned that off by default and it throws unless the compiler is run with
javascriptEnabled. The content is preserved unchanged, and the report warns you when it finds any, because that code is usually the reason an old stylesheet stopped building.
Where this page stops
- It formats, it does not compile. Variables stay as variables and mixin calls stay as calls. To see the CSS a browser would receive, run the file through LESS to CSS or the LESS compiler.
- It does not validate. A missing brace formats into neat, deeply indented nonsense rather than an error. If the output suddenly looks far more nested than the source, an unclosed block is the usual cause.
- Imports are invisible. An
@importline is counted and left alone. Variables and mixins defined in the imported file are unknown here, so the definition count only covers what you pasted. - Blank lines inside blocks are collapsed. Deliberate spacing between groups of declarations does not survive. Only the separation between top level rules is preserved, and only when the checkbox is on.
- Comment position is approximate. Block comments spanning several lines are placed on their own lines at the current indent. A trailing comment sitting after a declaration usually stays put, but one wedged into the middle of a selector will move.
- No source maps, no minification. This is a one way formatting pass. For the opposite direction use the CSS minifier on the compiled output.
