Skip to content

fix(native): seed a concrete scheme-aware __rn-css-color on every platform - #392

Open
YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/root-color-seed
Open

YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/root-color-seed

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Every color that falls back to the root __rn-css-color paints nothing where the platform reads it without resolving a semantic color — everywhere on Android, and on iOS for border, outline and shadow. Tailwind v4's default ring color is currentcolor, so ring, inset-ring, text-current and border-current are the everyday triggers.

rootVariables("__rn-css-color").set([[
  Platform.OS === "ios"
    ? PlatformColor("label", "labelColor")
    : PlatformColor("?attr/textColorPrimary", "SystemBaseHighColor"),
]] as any);

Root cause

  • Android. ?attr/textColorPrimary is a ColorStateList, and ColorPropConverter.resolveThemeAttribute returns outValue.data without checking outValue.type, so the paint receives a resource reference rather than a color.
  • iOS. A semantic UIColor has to be resolved against a trait collection. RCTViewComponentView.mm does that for the background alone; border, outline and shadow read .CGColor directly (facebook/react-native#57836).

Solution

The seed is color's initial value, CanvasText (CSS Color 4 §3.2), made concrete: black, or white under prefers-color-scheme: dark. The root observable already evaluates a seeded value's media query against colorScheme, so the seed stays reactive with no Platform.OS branch, no Appearance listener, no as any and no eslint suppression. An ancestor's color or a :root color still overrides it.

Tests

  • src/__tests__/native/root-color-seed.test.tsx — each of 13 currentcolor consumers compiles to the root color reference and paints #000000 in a light scheme and #FFFFFF in a dark one, and so does a currentcolor held in a custom property the compiler keeps; no preference, a live scheme change, an ancestor color, a :root color and a scheme-conditioned :root color are pinned as well.
  • src/__tests__/vendor/tailwind/current-color.test.tsx — Tailwind's own currentcolor utilities (text-current, bg-current, border-current, outline-current, decoration-current, caret-current, fill-current, stroke-current, ring, inset-ring, shadow-current, placeholder:text-current) in both schemes, and an explicit ring color over the default.
  • src/__tests__/native/box-shadow.test.tsx — a platformColor() shadow still reaches the shadow as the platform's object.
  • Four suites that pinned the unresolved PlatformColor descriptor for a currentcolor reader now assert the painted color.

On main, 59 cases fail with the PlatformColor seed and all of them pass with this one.

Two expectations follow what main does: it maps border-inline-start-color to borderStartColor, and writes a color-mix() shadow color as rgba().

Verification

On this head, the pull request's CI jobs run locally on Ubuntu with Node 22.18.0: yarn build, yarn lint, yarn typecheck, yarn test --maxWorkers=2 --coverage 1413 passed, node --test node/__tests__/*.mjs 42 passed, yarn example expo export --platform web, and no unstaged files after the build or the export. The iOS dev build, run with Xcode 27.1 where CI pins 26.6, fails on main as on this head, before any JavaScript is bundled: Expo's ExpoModulesJSI does not build with it (cannot find type '__ObjC::expo' in scope).

Device evidence, before and after: #392 (comment)

Open questions

?attr/colorForeground points at a plain <color> in AppCompat and Material, so it would resolve correctly and keep a custom brand theme's foreground instead of pure black or white. I chose the concrete pair because it is what CSS specifies and what light-dark() compiles to here; switching needs a device check I have not run.

Known limits

Related

#410 made the root registries process-global while colorScheme stays per module copy, so the seed's media query reads the colorScheme of whichever copy created the registries; #427 shares reactivity.ts's state across the split the same way.

Merge order

This overlaps #412, #494 and #498 in src/native-internal/root.ts and #489 in src/native/styles/shorthands/box-shadow.ts, and #391, #412 and #489 add cases that expect main's PlatformColor seed; I'll re-cut whichever lands after the others.

@YevheniiKotyrlo

YevheniiKotyrlo commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — The swatch is invisible or black — the seed is a PlatformColor Android cannot resolve to a value.

FIXED — The currentColor swatch is outlined exactly like the control, in the scheme’s foreground.

before — 3.1.0-rc.0 after — with this PR

Android 16 emulator (API 36, GLES on the host GPU), 1140×2400 @ 480dpi, both frames from one run. before is the published 3.1.0-rc.0 plus what my test app needs from my other patches to run: the native and native-internal subpath exports, PR 459, cascade-layer ranking, and a shim that compiles a class-qualified :root inert. after adds the rest of my open PRs, this one included. One of them shows on every screen: PR 417 narrows a var()-valued font-family to one family, so the after sets its text in the theme's face where the before falls back to the platform's.

…tform

The root seed is `color`'s initial value, `CanvasText`: black, or white in
a dark scheme. A semantic `PlatformColor` fallback paints nothing on the
border, ring and shadow paths of either platform, so the seed is a
concrete color chosen by the root's own `prefers-color-scheme`
evaluation. Every currentcolor consumer is pinned on the compile-time and
runtime paths in both schemes, from raw CSS and from Tailwind's own
utilities.
With inlineVariables off, `--c: currentcolor` stays a runtime variable
that references `__rn-css-color`, so `border-color: var(--c)` reaches
the seed through the runtime's variable lookup rather than the
compiler's inlined reference.

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