Repository navigation
fix: resolve light-dark() at runtime from the used colour scheme - #420
Open
YevheniiKotyrlo wants to merge 4 commits into
Open
YevheniiKotyrlo wants to merge 4 commits into
YevheniiKotyrlo wants to merge 4 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 17:15
This was referenced Aug 15, 2026
YevheniiKotyrlo
added a commit
to YevheniiKotyrlo/react-native-css
that referenced
this pull request
Aug 15, 2026
The pinned divergence is that PR's defect 2, not a new finding, and the double parse this branch fixes is its defect 6 — so the comment names the owner and the merge order rather than implying either is unclaimed.
Contributor
Author
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.
CSS Color 5 §7 makes `light-dark()` one value with two arms, resolved against the used colour scheme. It compiled to two rules instead: the current rule with the light arm and an extra rule under `prefers-color-scheme: dark` with the dark one. Wherever the dark rule could not reproduce the declaration it came from, dark mode broke: - a second `light-dark()` re-asserted the first one's light value, and the dark rule published the light colour to `currentcolor`; - a shorthand wrote its dark colour under the shorthand's name, so `box-shadow` became a bare colour and `border` gained a `border` key; - a custom property holding `light-dark()` kept its light value in dark mode wherever the inliner did not fold it; - inside a pseudo-element, the dark arm landed on the element. `light-dark()` now compiles to a `lightDark` value the runtime resolves against the colour scheme where it stands, so a shorthand, a custom property, a pseudo-element and the colour a rule publishes all carry it unchanged. The extra-rule mechanism had no other user and is removed.
CSS Color 5 §7 resolves light-dark() against the element's used colour scheme, which CSS Color Adjust 1 §2.3 derives from color-scheme: the preferred scheme when the element supports it, the one it supports otherwise, and light when it supports none. Chromium, Firefox and WebKit render it that way. color-scheme now compiles to a custom property the element publishes to its subtree, and the lightDark resolver reads it beside the preferred scheme, so a light-dark() declaration resolves with the variables in scope. BREAKING CHANGE: light-dark() renders its light colour unless color-scheme supports dark. Declare `color-scheme: light dark` on :root for light-dark() to follow the preferred scheme, as browsers require.
YevheniiKotyrlo
force-pushed
the
fix/light-dark-extra-rule
branch
from
October 11, 2026 12:35
ece47f8 to
bd350ee
Compare
This was referenced Oct 11, 2026
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
light-dark()compiles to two rules: the current rule with the light arm, and an extra rule underprefers-color-scheme: darkwith the dark arm. Wherever that second rule cannot reproduce the declaration it came from, dark mode renders wrong:.p { color: light-dark(red, blue) }publishes the light colour, socurrentcolorbelow it stays red; a secondlight-dark()on the same rule re-asserts the first one's light value.box-shadow: 0 0 1px light-dark(#123456, #654321)becomesboxShadow: "#654321"in dark mode, andborder/text-shadowgain a stray key: the dark arm is written under the shorthand's name.:root { --x: light-dark(#123456, #654321) }read throughvar(--x)stays light in dark mode unless the inliner folds it, and so does the same property declared on an element or an ancestor.::placeholder/::selectionthe dark arm lands on the element.color-schemeis dropped, solight-dark()follows the preferred scheme everywhere: dark with nocolor-schemedeclared, where browsers render the light arm, and light undercolor-scheme: dark.Root cause
CSS Color 5 §7 makes
light-dark()one value with two arms, resolved against the element's used colour scheme, which CSS Color Adjust 1 §2.3 derives fromcolor-scheme. A second rule gated on a media query copies the declaration's effect rather than its value, and the copy is wrong for every declaration that does more than write one property. The media query also reads the preferred scheme, never the element's.Separately, the inliner walks
varandfunctiontokens but not lightningcss'sunresolved-color, so a variable declared once lost its declaration while a read insidehsl(… / var(--a)),rgb(…)orlight-dark(…, var(--b))kept naming it.Solution
light-dark()compiles to alightDarkvalue holding both arms. A shorthand, a custom property on:root,*or an element, the colour a rule publishes and a pseudo-element's prop all carry it unchanged.color-schemecompiles to--__rn-css-color-scheme, published to the subtree the waydirectionis. The runtime picks the arm for the used colour scheme: the preferred scheme when the element supports it, the one it supports otherwise, and light when it supports none. Alight-dark()declaration is marked as reading variables, so it resolves with the inherited ones and subscribes to the preference like aprefers-color-schemequery.addExtraRule,extendRule,createRuleFromPartial,addUnnamedDescriptor,descriptorProperty) had no other user and is removed.light-dark()'s arms.Breaking: with no
color-scheme,light-dark()renders its light colour in dark mode, as Chromium, Firefox and WebKit do.:root { color-scheme: light dark }, Tailwind'sscheme-light-dark, makes it follow the preferred scheme. TheBREAKING CHANGE:footer is on the last commit.Tests
src/__tests__/native/light-dark.test.tsxderives every expectation. Each scene, with inline variables on and off, renders exactly as the same stylesheet with everylight-dark(L, D)replaced by the arm it should take.color-scheme: light dark. They covercolor,background-color, thebordershorthand, four border colours, the inline border colours, one border side, the block border, the inline-start border,outline-color,text-decoration-colorand its shorthand,box-shadowandtext-shadow. They also cover a nestedlight-dark(), avar()fallback, a custom property on:root,*, an ancestor and the element,currentcolorfrom an ancestor, two declarations on one rule, one and two variable arms,!importantand a media query.color-scheme,normal,light dark,dark light,only light dark,only light,only dark, an element'slightordark, inheritance andnormalunder a dark ancestor. They also cover a custom ident alone and besidedark, cascade order,!important, a media query,var()with and without a fallback,initial, a universalcolor-scheme, and a custom property resolved where it is used.::placeholder, and a stylesheet whoselightDarklacks an arm.src/__tests__/compiler/light-dark.test.tschecks one rule holding both arms for each declaration kind, the published colour, a:rootvariable, a variable arm'sdv, a media query's own condition, a pseudo-element's prop, and that alight-dark()declaration reads variables. It also checks whatcolor-schemepublishes for each keyword form, avar()and:root.src/__tests__/compiler/compiler.test.tsxadds folding cases inside anhsl()/rgb()alpha and alight-dark()arm, and its existinglight-dark()case pins the new shape.On
main, 96 of the 127 cases in the first two 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 lintclean ·yarn typecheckclean ·yarn test --maxWorkers=2 --coverage1461 passed, 4 failed — the babel cases that also fail onmainon this machine ·yarn buildclean ·yarn example expo export --platform webexported · nothing unstaged after either build. Not run: the iOS build.No open issue covers it.
Known limits
color-scheme: inherit,unsetandrevertthe same asnormal, so they take the light arm where browsers keep the parent's scheme..p { color-scheme: dark; color: light-dark(L, D) } .c { color-scheme: light; background-color: currentcolor },.cpaints L where browsers keep the parent's D.border-block-color: light-dark()writes the edge pair, while a plain colour collapses ontoborderBlockColor; fix: map every logical border property onto a prop React Native reads #393 gives every value the edge pair.Related
#411 scopes the extra rule
light-dark()opens inside a pseudo-element; with no extra rule, that part of #411 falls away in whichever lands second.Merge order
This overlaps #389, #391, #411 and #413; I'll re-cut whichever lands after the others.
It also shares lines with #393 (
compiler/declarations.ts,compiler/stylesheet.ts), #417 (compiler/declarations.ts); whichever lands second rebases.Base
Re-written on
main(a5002c5). It replaces the extra-rule rework this PR carried, which fixed the first and fourth bullets above but not the shorthands, the custom properties orcolor-scheme.