Repository navigation
fix(compiler): an unresolved alpha corrupts the colour beside it - #421
Open
YevheniiKotyrlo wants to merge 3 commits into
Open
YevheniiKotyrlo wants to merge 3 commits into
YevheniiKotyrlo wants to merge 3 commits into
Conversation
YevheniiKotyrlo
marked this pull request as draft
August 15, 2026 14:35
YevheniiKotyrlo
marked this pull request as ready for review
August 15, 2026 19:13
Contributor
Author
lightningcss hands over the channels of `rgb(… / var(--a))` as 0-255 integers, and parseUnresolvedColor multiplied them by 255 again, so `rgb(255 0 0 / var(--a))` rendered `rgba(65025, 0, 0, 0.5)`. The hsl spelling emitted `hsl(120, 50, 50, 0.5)`, which React Native rejects. The rgb channels pass through, and the hsl channels convert to the sRGB parseColor writes for the resolved spelling. A hue that is infinite, NaN or past the float32 range of the second lightningcss pass renders as hue 0, as browsers render an infinite or NaN hue.
lightningcss parses `hsl(0 100% 50% / var(--a))`, `rgb(… / var(--a))` and `light-dark(nativewind#123, var(--b))` as an unresolved colour whose channels are token lists, and the inliner walked `var` and `function` tokens only. A variable declared once therefore lost its declaration while a read inside a colour function kept naming it, so the alpha, or the arm, came out empty.
Each colour is the value Chromium 153, Firefox 155 and WebKit 26.6 compute with an alpha of 0.5, and each is rendered four ways: the alpha read at runtime, read from a fallback, folded at compile time, and the resolved spelling. A hue that is infinite or NaN renders as hue 0 in all three browsers; lightningcss's first pass resolves the resolved spelling of one to an achromatic colour, which stays pinned.
YevheniiKotyrlo
force-pushed
the
fix/unresolved-color-channel-scale
branch
from
October 11, 2026 12:35
8c5c581 to
d91f671
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
lightningcss hands over
rgb(… / var(--a))andhsl(… / var(--a))as an unresolved colour: the channels resolved, the alpha a token list. Onmainboth spellings render the wrong colour or none:rgb(51 102 153 / var(--a))rendersrgba(13005, 26010, 39015, 0.5), which React Native clamps to white. Only a saturated channel such asrgb(255 0 0 …)comes out right.hsl(120, 50, 50, 0.5), which React Native's parser rejects, so the property is unset. So is every infinite or NaN hue.Root cause
parseColordivides the same channels by 255 for colorjs.io.parseUnresolvedColormultiplied them again.hsl, while React Native'shsl()takes three arguments with percentage saturation and lightness.varandfunctiontokens but not lightningcss'sunresolved-color.Solution
parseColorconverts the resolved spelling, and emitrgba(r, g, b, alpha).2 ** 32renders as hue 0. Chromium, Firefox and WebKit render an infinite or NaN hue as 0. The second lightningcss pass reparses a hue as a float32, and from2 ** 32one float32 step spans more than a turn, so the value no longer names an angle.light-dark()'s arms. fix: resolve light-dark() at runtime from the used colour scheme #420 carries the same commit; whichever lands second drops it.Tests
src/__tests__/native/colors.test.tsxdeg,turn,radandgrad, with wrapped and negative hues and clamped saturation and lightness3e9reduces;2 ** 32,1e20and1e38render as hue 0, as their resolved spelling does.light-dark()carrying an unresolved alpha into both schemes.src/__tests__/compiler/declarations.test.tsx: the descriptor for each spelling.src/__tests__/compiler/compiler.test.tsx: folding inside an alpha and inside alight-dark()arm.On
main, 83 of the 144 cases in the three files fail; all pass here. Each mechanism was reverted on its own to confirm its cases fail without it.Verification
On Windows with Node 26:
yarn lintcleanyarn typecheckcleanyarn test --maxWorkers=2 --coverage: 1442 passed, 4 failed. The 4 are the babel cases that also fail onmainon this machine.yarn buildcleanyarn example expo export --platform webexportedNot run: the iOS build.
No open issue covers it. Device evidence, before and after: #421 (comment)
Known limits
background-color, inherited forcolor. The cause is that the runtime drops avar()with no value from the function it sits in, and that reaches every function, not only colours.--a: 25%reaches it asrgba(255, 0, 0, 25%))nonechannelhwb()hsl()hsl(12345678 50% 50% / var(--a))arrives ashsl(12345700 …)and renders 220° where the resolved spelling and browsers render 198°.3e9, Chromium and WebKit reduce it to 120° and Firefox renders 0°; this follows Chromium and WebKit.hsl(calc(infinity) 100% 50%)to#000, while browsers and the unresolved spelling render hue 0.Related
#420 carries the same inliner commit. #351 fixed the same class of defect in the same file.
Merge order
This overlaps #391; I'll re-cut whichever of the two lands second.
Base
Re-written on
main(a5002c5).