Skip to content

fix(compiler): list a stylesheet's variables in the same order on every read - #476

Open
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/stylesheet-variable-order
Open

YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/stylesheet-variable-order

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Problem

getNativeStyleSheet reverses each root and universal variable list in place to put the most specific value first. Reading the stylesheet a second time reverses the lists back. Measured on main:

const compiled = compile(
  `:root { --tone: red; }
   @media (prefers-color-scheme: dark) { :root { --tone: blue; } }`,
  { inlineVariables: false },
);
compiled.stylesheet().vr; // [["tone", [["blue", [["=", "prefers-color-scheme", "dark"]]], ["red"]]]]
compiled.stylesheet().vr; // [["tone", [["red"], ["blue", [["=", "prefers-color-scheme", "dark"]]]]]]

The second read lists the unconditioned value first, so the dark value never wins, and the first sheet's lists change under it because both reads hand out the builder's own arrays. The same holds for vu.

registerCSS reads the stylesheet twice when debug is on, logging one read and injecting the next, so on main a debug run renders a dark-mode variable's light value.

Solution

Both lists are reversed into a copy with toReversed(), so every read of a stylesheet is the same and a sheet already handed out stays as it was.

Tests

  • src/__tests__/compiler/compiler.test.tsx: reading the stylesheet twice gives the same vr and vu, most specific value first, and leaves the first read unchanged.
  • src/__tests__/native/selectors.test.tsx: after a second read of the stylesheet is injected, a :root and a * variable render their dark value in dark mode and their light value otherwise, without inlineVariables and with inlineVariables: false. Chromium 156, Firefox 157 and WebKit 27.2 render the same stylesheet the same way: the dark rule comes later, so it wins while prefers-color-scheme: dark holds (css-cascade-5 §6.1, mediaqueries-5 §12.5).
  • Without the fix, the compiler case fails at the second read and both render cases render the light value in dark mode, while every other case in the two files passes. Restoring the in-place reverse on either list fails all three, and dropping the reverse on vr also fails :root CSS variables with media queries and root variables track system dark mode.

Verification

Every ci.yml job on this branch merged into main (2421ea77) passes: lint, test, build-library, build-web and commitlint on Ubuntu 26.04 with Node 26.12.1, where .nvmrc pins 22.18.0, and build-ios-dev on macOS with Xcode 27.1, where the workflow pins 26.6.

No existing issue tracks this. I searched for getNativeStyleSheet, rootVariables reverse and variables order media.

Both reads and a debug render, on main and here: #476 (comment)

@YevheniiKotyrlo

YevheniiKotyrlo commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

A stylesheet read twice — main against this PR

One :root variable, --tone, red with a blue override under prefers-color-scheme: dark. I read its compiled root-variable list, vr, twice through compile(), and rendered it in dark mode through registerCSS with debug: true, which reads the stylesheet once to log it and again to inject it. The runtime applies a variable's first entry whose condition holds, so the dark value has to come first.

main (2421ea77) this PR (54a0b306) merged into main
first read's vr [["tone",[["blue",[["=","prefers-color-scheme","dark"]]],["red"]]]] the same: dark value first
second read's vr [["tone",[["red"],["blue",[["=","prefers-color-scheme","dark"]]]]]] the same as the first read
first read's vr, looked at again after the second read changed to the second read's order the same as before the second read
color rendered in dark mode, without inlineVariables and with inlineVariables: false red blue

Chromium 156, Firefox 157 and WebKit 27.2 render this stylesheet blue in dark mode and red in light mode.

const compiled = compile(
  `:root { --tone: red; }
   @media (prefers-color-scheme: dark) { :root { --tone: blue; } }`,
  { inlineVariables: false },
);
const first = compiled.stylesheet();
JSON.stringify(first.vr);
JSON.stringify(compiled.stylesheet().vr);
JSON.stringify(first.vr);
registerCSS(
  `:root { --tone: red; } @media (prefers-color-scheme: dark) { :root { --tone: blue; } } .my-class { color: var(--tone); }`,
  { debug: true }, // and again with { debug: true, inlineVariables: false }
);
act(() => {
  colorScheme.set("dark");
});
render(<View testID={testID} className="my-class" />);
screen.getByTestId(testID).props.style;

I ran each snippet as a jest case in src/__tests__/native/ on Windows with Node 26. They are not part of the PR; its own cases are in compiler.test.tsx and selectors.test.tsx.

…ry read

getNativeStyleSheet reversed each root and universal variable list in
place to put the most specific value first. Reading the stylesheet again
reversed the lists back, so a second read listed the unconditioned value
first, and the first sheet's lists changed under it.

The lists are now reversed into a copy.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/stylesheet-variable-order branch from 622a46e to 54a0b30 Compare October 28, 2026 23:38

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant