Migrating Less 4.x → 5.x
Coming to Jess from Less 4.x? Jess is the engine behind Less 5.x, so the same breaking changes apply when you move existing Less sources onto Jess. This page lists the ones that most often need attention.
Removed: inline backtick JavaScript
Inline backtick JavaScript is removed entirely. It now reports a fatal unsupported-syntax diagnostic — not a flag you can opt back into.
// No longer valid — this reports an unsupported-syntax diagnostic:
@columns: `Math.max(12, 8) `;
In Jess there is no inline-JavaScript syntax at all. Express values with
built-in functions where possible (@columns: max(12, 8);). Script and data imports use @-use / @-from; see
Modules & imports.
Imports
Jess's module system is @-compose (namespaced import, isolated scope, optional
as alias). In .jess a bare @import is always a plain CSS
at-rule (passed through — no extension heuristic); it is never a source include.
To fold a stylesheet in and evaluate it, use an explicit dash-prefixed rule:
@-compose (preferred) or @-import (mechanical port; warns). The compiler
at-rules require the dash — bare @use/@compose are not valid Jess.
// CSS @import (passed through), NOT a source include:
@import "foo";
// Legacy source include (warns) — prefer @-compose:
@-import "./foo.less";
// The module system:
@-compose "./foo.less" as foo;
See Modules & imports.
CSS nesting is preserved by default (collapseNesting: false)
Less 5.x has first-class CSS nesting, and it keeps nested structure by default
(collapseNesting: false). Output stays in the familiar nested shape unless you
explicitly opt into collapsing.
.card {
padding: 1rem;
.title {
font-weight: 600;
}
}
By default this compiles to nested CSS (.title stays nested under .card).
Enable collapseNesting: true for the older flattened (.card .title { … })
output when you need deduplication or wider tooling support.
If you are porting a 4.x project that assumed flattened output, set
collapseNesting: true while you migrate, then move to native nesting once your
toolchain supports it.
Less-style parent-suffix composition still works, and the parent-template model is now explicit:
&-1composes a suffix onto the parent (.col { &-1 {…} }→.col-1).&()keeps the parent selector but hoists the nested selector to root.&('')drops the parent selector entirely.&(-1)is equivalent to&-1.
At-rule variables require interpolation
Where a variable supplies syntax rather than a value, Less 5.x requires
explicit @{…} interpolation. Less 4.x still accepts the bare spelling but emits
a deprecation warning for it.
@breakpoint: (min-width: 48rem);
@animation-name: fade-in;
// Deprecated in 4.x, removed in 5.x:
// @media @breakpoint { … }
// @keyframes @animation-name { … }
// Less 5.x:
@media @{breakpoint} {
.card {
animation-name: @animation-name;
}
}
@keyframes @{animation-name} {
from { opacity: 0; }
to { opacity: 1; }
}
This applies wherever a variable becomes part of at-rule syntax — @media,
@supports, @container, @layer, @keyframes, @namespace — and it is the
same rule that already governs selectors (.@{name}), property names
(@{name}: value), and @import paths.
Variables in value positions are unaffected and keep the bare spelling. That includes ordinary declaration values and media feature values:
@sizes: {
tablet: 768px;
};
@media (min-width: @sizes[tablet]) {
.navbar {
display: inline-block;
}
}
extend and the !all flag
extend behavior is validated against fixture parity, including nested and
media-scoped selectors. Per-selector all in multi-target extends is deprecated
in favor of a single !all flag:
// Deprecated:
&:extend(.a all,
.b all);
// Preferred:
&:extend(.a,
.b !all);
With collapseNesting: true, extend … !all in nested selector-list cases may
emit :is(…) selectors to reduce duplication.
Variable resolution follows source order
Less 4.x had eager-resolution edge cases where a later mixin call could appear to retroactively change an earlier declaration. Less 5.x resolves declarations in source order against the scope that exists when they are evaluated — a later side effect does not rewrite an already-evaluated sibling declaration.
@mix: blue;
.mixin() {
@mix: #989;
}
.tiny-scope {
color: @mix; // blue — resolved before .mixin() runs
.mixin();
}
Math modes
- Move
strictMathworkflows to themathoption. strict-legacymath mode is removed.math=alwaysis deprecated; treat it as legacy behavior.
# old
lessc --math=always styles.less styles.css
# preferred
lessc --math=parens-division styles.less styles.css
Evaluation differences (comparison, truthiness, arguments)
The changes above are structural — features that moved or were removed. This section is the smaller, more important set: input that compiles in both Less 4 and Less 5 and evaluates to something different.
The bar for changing an evaluation result is high. Less 5 follows Less 4 unless the Less 4 behaviour is defensible by history rather than by principle — meaning it does something documented or long-standing, but has no rule that explains why. Every row below states the rule Less 5 follows instead.
| expression | Less 4 | Less 5 | why |
|---|---|---|---|
1in = 2.54cm | false | true | Equal by definition. Less 4's false was a conversion-precision defect, not a language choice. |
b > a | false | true | Relational is now trichotomous. Less 4 answered false in both directions, so an author could not tell "not greater" from "never comparable". |
1px > red | false | error | No common ground. Ordering a length against a keyword is an author mistake, and Less 4 answered it meaninglessly. |
a = "a" | false | true | A value equals its own spelling. Less 4 separated text by quoting while coercing numbers — the reverse of Sass, and explicable as neither. |
(2px / 1px) | 2px | 2 | Like units cancel. A ratio of two lengths is a number; Less 4 re-attached a unit the value no longer carried. |
(1px * 2px) | 2px | calc(1px * 2px) | An area has no CSS unit. Less 4 kept the left operand's unit, which is dimensionally false. See Unit algebra below. |
(1 / 2px) | 0.5px | calc(1 / 2px) | There is no px⁻¹ in CSS. Same rule as the row above. |
when (~"true") | truthy | falsy | See Truthiness below — the one row where Less 4 promoted a string to truthy. |
not(1 > 2), and(…), or(…) | parse error | true | The logical operators are real operators in Less 5, not call-shaped syntax. |
foo(bar=@v) | foo(bar=50) | foo(bar=@v) | A name=value argument is verbatim bytes. The IE-filter form is dropped in Less 5, and resolving inside it had no purpose. |
@import "x.less" screen; | wraps in @media | parse error | A media postlude belongs to a CSS @import. On a compile-time import it is bogus, and the shape is now decided at parse time. |
Truthiness is unchanged — with one exception
In Less, nothing is truthy except the boolean true (or an expression that evaluates to it, like 1 > 0). That is Less 4's rule and Less 5 keeps it. A guard takes a condition; anything else needs an explicit comparison.
Note this is not the Jess / Sass+ rule, where a non-empty string is truthy. Do not carry that intuition into .less.
| guard | Less 4 | Less 5 |
|---|---|---|
when (true), when (1 > 0) | truthy | truthy |
when (1), when (0), when (1px) | falsy | falsy |
when ("a"), when (""), when (~"a") | falsy | falsy |
when (red), when (abc) | falsy | falsy |
when (~"true") | truthy | falsy |
Only the last row moves, and it moves toward the rule. ~"true" is a string, and strings are not truthy in Less — ~"a" is falsy in both versions. Less 4 made an exception for this one string because it re-parsed the escaped bytes back through evaluation and handed the guard the keyword true. Its contents decided the outcome, which is exactly what the rule says must not happen. Less 5 removes the exception.
If you were relying on when (~"true"), write when (true).
Unit algebra: a unit CSS cannot spell changes the spelling, not the arithmetic
Multiplying or dividing two united operands can produce a unit CSS has no name
for — 1px * 2px is an area, 1 / 2px is a reciprocal length. Less 4 invented a
unit for these, keeping the left operand's (1px * 2px → 2px). That answer
looks plausible and is dimensionally false.
Less 5 keeps the number and the units, and only changes how the result is
written: an unexpressible result is composed as calc(…), in the operand order
you wrote. The arithmetic always happens. That is what lets a chain recover
the moment a later operation cancels back to a real unit:
| expression | Less 4 | Less 5 |
|---|---|---|
(2px / 1px) | 2px | 2 |
(1px * 2px) | 2px | calc(1px * 2px) |
(1 / 2px) | 0.5px | calc(1 / 2px) |
(10% * 1px) | 10% | calc(10% * 1px) |
(1px * 1px / 1px) | 1px | 1px |
(8cats * 9dogs / 4cats) | 18dogs | 18dogs |
unit((1px * 4em / 2cm)) | 2 | 2 |
The last three rows are the point: they are unchanged. 1px * 1px has no CSS
spelling, but it is still a computed value carrying 1 and the units px·px, so
dividing by 1px cancels back to an honest 1px. Only a result that is
emitted while still unexpressible gets the calc(…) spelling.
An unexpressible result also warns (eval/unexpressible-unit), and the
unitMode option decides what happens to it:
unitMode | (1 / 2px) | |
|---|---|---|
loose | 0.5px | Less 4's answer, plus a warning |
preserve (default) | calc(1 / 2px) | plus a warning |
strict | rejected | set by the strict preset |
No mode is silent. Silent preservation is the worst outcome — the stylesheet looks fine and you never learn the expression was meaningless.
The canonical statement of this rule is §4.7 of
docs/design/RESOLVED-SEMANTICS-AND-NAMING.md in the repository.
A guard selects; it does not assert
The 1px > red row above is about a value: @x: (1px > red) has no answer,
so it errors. In a guard the same comparison does not error — the definition
simply does not match, and overload resolution moves on:
.generic(@a, @b) when (@a < @b) { r: less; }
.generic(@a, @b) when (@a > @b) { r: greater; }
.generic(@a, @b) { r: fallback; }
a { .generic(1, true); } // r: fallback — neither guard matches, nothing errors
This is unchanged from Less 4, which answers r: fallback here as well. It
is called out because the value-position rows above might otherwise suggest a
guard would now fail the compile. It does not: a guard asks does this definition
apply?, and "these operands share no ground" is a perfectly good "no".
This is not a mode the evaluator switches on. The two positions are lowered to
two different nodes when the file is parsed — a comparison in a value, a match
in a guard — over one shared ground computation and one shared answer table, so
the two positions cannot drift apart. See §4.2a of
docs/design/RESOLVED-SEMANTICS-AND-NAMING.md.
Unaffected, and worth knowing
An explicit comparison is never affected by the truthiness change:
.m() when (@name = "") { … } // identical in Less 4 and 5
.m() when (@name) { … } // truthiness — see the table
Numeric comparison, unit conversion between compatible units, and colour equality all behave as before. @import of a .css file, url(…) imports and (inline) are unchanged.
Not yet changed
Math-function preservation (min(100% - 30px) returning the expression rather than folding to 70%) is specified for Jess but not yet wired into .less — Less 5 still folds it, matching Less 4. If you rely on the fold, nothing has moved.
Deprecated CLI / option paths
--relative-urls→--rewrite-urls=all(orrewriteUrls).--ie-compatis a no-op in modern pipelines.- built-in
compress→ use a dedicated CSS minifier. dumpLineNumbers/--line-numbers→ use sourcemaps.strictImportsis deprecated; avoid it in new configs.
Runs on the Jess engine
Jess is the engine. Less 5.x is Jess with the Less-compatibility surface turned on, so the evaluation model described throughout these docs is exactly what your migrated Less sources run against.