Skip to main content

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:

  • &-1 composes 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 strictMath workflows to the math option.
  • strict-legacy math mode is removed.
  • math=always is 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.

expressionLess 4Less 5why
1in = 2.54cmfalsetrueEqual by definition. Less 4's false was a conversion-precision defect, not a language choice.
b > afalsetrueRelational is now trichotomous. Less 4 answered false in both directions, so an author could not tell "not greater" from "never comparable".
1px > redfalseerrorNo common ground. Ordering a length against a keyword is an author mistake, and Less 4 answered it meaninglessly.
a = "a"falsetrueA value equals its own spelling. Less 4 separated text by quoting while coercing numbers — the reverse of Sass, and explicable as neither.
(2px / 1px)2px2Like units cancel. A ratio of two lengths is a number; Less 4 re-attached a unit the value no longer carried.
(1px * 2px)2pxcalc(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.5pxcalc(1 / 2px)There is no px⁻¹ in CSS. Same rule as the row above.
when (~"true")truthyfalsySee Truthiness below — the one row where Less 4 promoted a string to truthy.
not(1 > 2), and(…), or(…)parse errortrueThe 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 @mediaparse errorA 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.

guardLess 4Less 5
when (true), when (1 > 0)truthytruthy
when (1), when (0), when (1px)falsyfalsy
when ("a"), when (""), when (~"a")falsyfalsy
when (red), when (abc)falsyfalsy
when (~"true")truthyfalsy

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:

expressionLess 4Less 5
(2px / 1px)2px2
(1px * 2px)2pxcalc(1px * 2px)
(1 / 2px)0.5pxcalc(1 / 2px)
(10% * 1px)10%calc(10% * 1px)
(1px * 1px / 1px)1px1px
(8cats * 9dogs / 4cats)18dogs18dogs
unit((1px * 4em / 2cm))22

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)
loose0.5pxLess 4's answer, plus a warning
preserve (default)calc(1 / 2px)plus a warning
strictrejectedset 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 (or rewriteUrls).
  • --ie-compat is a no-op in modern pipelines.
  • built-in compress → use a dedicated CSS minifier.
  • dumpLineNumbers / --line-numbers → use sourcemaps.
  • strictImports is 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.