Skip to content

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

Open
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/root-color-seed
Open

YevheniiKotyrlo wants to merge 1 commit 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; 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, 56 cases fail with the PlatformColor seed and all of them pass with this one.

Verification

On Windows with Node 26: yarn lint clean · yarn typecheck clean · yarn test --maxWorkers=2 --coverage 1401 passed, 4 failed — the babel cases that fail on main on this machine · yarn build clean · yarn example expo export --platform web exported · nothing unstaged after either build. Not run: the iOS dev build.

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

Open question

?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.

Related

#410 makes the root registries process-global while colorScheme stays per module copy, so a media-conditioned seed binds to whichever copy evaluated first; #427 guards reactivity.ts's observables the same way. Both edit src/native-internal/root.ts.

Merge order

This overlaps #410; I'll re-cut whichever of the two lands second.

It also shares lines with #412 (native-internal/root.ts); whichever lands second rebases.

Base

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

@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.

@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(native): seed a concrete scheme-aware Android __rn-css-color fix(native): seed a concrete scheme-aware __rn-css-color on every platform Aug 20, 2026
…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.

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