CSS Color Level 5 Relative Color Syntax in Less#
CSS Color Level 5 introduced relative color syntax β e.g. rgb(from var(--surface) r g b / 0.9) β which lets a color function derive its channel values from an existing color. Less must pass these expressions through to the browser unchanged, but the colorOperand grammar rule and Less's division-operator handling create a conflict that produces compilation errors under certain math modes.
The colorOperand Grammar Rule#
PR #4346 (merged into v4.4.0, fixing #4343) added a colorOperand parser rule to packages/less/src/less/parser/parser.js. Without it, expressions like oklch(from blue l c h) or rgb(from blue calc(r + 1) g b) failed with Could not parse call arguments or missing ')' because the single-letter channel tokens (r, g, b, l, c, h, s) were not recognized as valid operands.
The rule matches the regex /^[lchrgbs]\s+/ and returns a tree.Keyword node for the matched channel letter . This enables the channel identifiers to participate in Operation nodes β which is exactly what makes calc(r + 1) work β but also causes the alpha-slash conflict described below.
PR #4434 (v4.6.6, fixing #4316) tightened the regex to /^([lchrgbs])(?=\s|[,/*)]|$)/ using a lookahead, so colorOperand also fires when a channel letter is followed by /, *, ,, or ) β which was necessary to handle calc() expressions inside from expressions . This tightening unintentionally made the alpha-slash bug worse: b now matches even when directly followed by /, ensuring b / 0.9 is always parsed as a Less Operation.
The Alpha-Slash / Division Conflict#
CSS relative-color syntax uses / to separate color channels from the alpha value:
rgb(from var(--surface) r g b / 0.9)
Less also uses / as a division operator. After colorOperand was introduced, the parser matches b as an operand; then the / is consumed as a division operator; the full sub-expression b / 0.9 becomes a Less Operation node.
At evaluation time, Operation.eval() calls context.isMathOn(this.op) :
- Under
--math=parens-division(the default),isMathOn('/')returnsfalseunless inside explicit parentheses, so theOperationis emitted as-is β no error. - Under
--math=always,isMathOn('/')returnstrue, so Less tries to perform the division.bevaluates to aKeywordwith no.operatemethod, hitting thethrow { type: 'Operation', message: 'Operation on an invalid type' }path .
The existing escape hatch in Operation.eval() only fires when context.math === MATH.PARENS_DIVISION, leaving --math=always without a fallback.
This is tracked in issue #4528 , reported on 2026-09-08 against Less 4.9.1. It is an open regression.
Behavior by Mode (summary table)#
| Input | --math=always | --math=parens-division |
|---|---|---|
rgb(from var(--x) r g b / 0.9) | Error (v4.4.0+) | Pass-through β |
rgb(from #112233 r g b / 0.9) | Error (v4.4.0+) | Pass-through β |
rgb(from var(--x) r g b) (no slash) | Pass-through β | Pass-through β |
rgb(17, 34, 51) (Less constructor) | #112233 β | #112233 β |
Analogous Fix Pattern: Math Functions (PR #4479)#
PR #4479 (v4.8.1) fixed the same class of problem for CSS math functions (sin(var(--x)), cos(var(--a)), etc.). The MathHelper was updated to return undefined β causing the function call to pass through to the browser β when its argument is an unresolvable Call node. The rgb()/hsl() color function path in color.js does not yet receive the same treatment .
Proposed Fix Approaches#
Two approaches have been identified :
Operation.eval()fallback β extend the pass-through escape hatch to also fire when operands are unresolvableKeyword/Callnodes, regardless of math mode.- Color function bail-out β detect the relative-color
fromkeyword insidergb()/hsl()functions and returnundefinedearly, mirroring theMathHelperapproach from PR #4479.
Workarounds (until fixed)#
- Quote the value to prevent Less from parsing it:
~'rgb(from var(--surface) r g b / 0.9)' - Use
--math=parens-division(or omit--math=always).
Key Source Files#
| File | Relevance |
|---|---|
parser/parser.js | operand and colorOperand grammar rules |
tree/operation.js | Operation.eval() β math mode gating and the throw path |
contexts.js | isMathOn() β controls whether / triggers evaluation |
functions/color.js | rgb() / hsl() function handlers |