Bug summary
With react-native-css@3.0.7, Expo 57 / React Native 0.86 / Metro 0.84 can bundle both dist/module and dist/commonjs copies of the native runtime. StyleCollection and VariableContext are globalized, but rootVariables / universalVariables are module-local.
The transformed CSS module is CommonJS and StyleCollection.inject() therefore closes over the CommonJS rootVariables family. Components rewritten from react-native use the ESM runtime, whose varResolver() reads the ESM family. The stylesheet rules are registered (because StyleCollection is shared), but any runtime var(...) resolves to undefined.
This makes direct/inlined utility values work while mode-aware CSS-variable-backed colors render transparent.
Version / environment
- react-native-css: 3.0.7
- nativewind: 5.0.0-preview.4
- Expo: 57.0.11
- React Native: 0.86.2
- Metro: 0.84.4
- pnpm: 11.17.0
- iOS simulator (same architecture should affect Android)
Runtime evidence
Using the React Native CDP inspector against the live app:
__r(337).StyleCollection === __r(1247).StyleCollection
// true
__r(337).rootVariables === __r(1247).rootVariables
// false
__r(337).rootVariables("color-surface-primary").get()
// undefined
__r(1247).rootVariables("color-surface-primary").get()
// "#fff"
Here module 337 is dist/module/native-internal/index.js, while 1247 is dist/commonjs/native-internal/index.js.
Both runtimes are present in the same bundle:
dist/module/native-internal/root.js
dist/commonjs/native-internal/root.js
dist/module/native/styles/variables.js
dist/commonjs/native/styles/variables.js
The CSS class itself is correctly registered:
globalThis.__react_native_css_style_collection
.styles("bg-utility-neutral-900")
.get()
// [{ d: [[[{ }, "var", "color-utility-neutral-900", 1], "backgroundColor", 1]], dv: 1, ... }]
Resolving the same rule through each runtime proves the split:
const get = observable => observable.get();
const rules = globalThis.__react_native_css_style_collection
.styles("bg-utility-neutral-900")
.get();
__r(730).calculateProps(get, rules).normal
// ESM calculate-props: { style: {} }
__r(1263).calculateProps(get, rules).normal
// CJS calculate-props: { style: { backgroundColor: "#171717" } }
The generated bundle shows the CSS injection module depending on the CommonJS native-internal module, while React Native wrappers and useNativeCss use ESM modules.
Why this happens
src/native-internal/style-collection.ts stores StyleCollection on globalThis, and variables.tsx similarly globalizes VariableContext. But src/native-internal/root.ts creates these per module instance:
export const rootVariables = rootVariableFamily();
export const universalVariables = rootVariableFamily();
So ESM/CJS duplication is survivable for registered styles but not variables.
Metro 0.84 intentionally resolves package conditional exports by source import vs require, so a package containing both forms can legitimately include both targets in one bundle.
Suggested fix
Make the root/universal variable families canonical across module formats, similar to StyleCollection and VariableContext (for example, store both on globalThis), and add a regression test/build fixture where CSS injection comes from the CommonJS export while rendering uses the ESM export.
A package-level alternative would be to expose one canonical React Native target above import/require in the exports map, but globalizing the variable registries would also defend against duplicate package instances.
Bug summary
With
react-native-css@3.0.7, Expo 57 / React Native 0.86 / Metro 0.84 can bundle bothdist/moduleanddist/commonjscopies of the native runtime.StyleCollectionandVariableContextare globalized, butrootVariables/universalVariablesare module-local.The transformed CSS module is CommonJS and
StyleCollection.inject()therefore closes over the CommonJSrootVariablesfamily. Components rewritten fromreact-nativeuse the ESM runtime, whosevarResolver()reads the ESM family. The stylesheet rules are registered (becauseStyleCollectionis shared), but any runtimevar(...)resolves toundefined.This makes direct/inlined utility values work while mode-aware CSS-variable-backed colors render transparent.
Version / environment
Runtime evidence
Using the React Native CDP inspector against the live app:
Here module
337isdist/module/native-internal/index.js, while1247isdist/commonjs/native-internal/index.js.Both runtimes are present in the same bundle:
The CSS class itself is correctly registered:
Resolving the same rule through each runtime proves the split:
The generated bundle shows the CSS injection module depending on the CommonJS
native-internalmodule, while React Native wrappers anduseNativeCssuse ESM modules.Why this happens
src/native-internal/style-collection.tsstoresStyleCollectiononglobalThis, andvariables.tsxsimilarly globalizesVariableContext. Butsrc/native-internal/root.tscreates these per module instance:So ESM/CJS duplication is survivable for registered styles but not variables.
Metro 0.84 intentionally resolves package conditional exports by source
importvsrequire, so a package containing both forms can legitimately include both targets in one bundle.Suggested fix
Make the root/universal variable families canonical across module formats, similar to
StyleCollectionandVariableContext(for example, store both onglobalThis), and add a regression test/build fixture where CSS injection comes from the CommonJS export while rendering uses the ESM export.A package-level alternative would be to expose one canonical React Native target above
import/requirein the exports map, but globalizing the variable registries would also defend against duplicate package instances.