From a2190a665eeca00b1f6373cac9200c60d946df00 Mon Sep 17 00:00:00 2001 From: Maxence Henneron Date: Mon, 20 Apr 2026 13:58:28 -0700 Subject: [PATCH 1/2] fix: propagate inlineRem and Metro config to compiler (Tailwind v4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs caused Tailwind v4's text utilities to render at the wrong size on native. Both manifest in v4 specifically and are benign on v3, because v3 emits direct `rem` values while v4 routes them through CSS custom properties. Bug 1 — Metro transformer drops `withReactNativeCSS` options ----------------------------------------------------------- `withReactNativeCSS()` writes its options to `config.transformer.reactNativeCSS`, which Metro surfaces as `config.reactNativeCSS` in the transformer. The transformer was instead spreading `options.reactNativeCSS`, a key that nothing ever writes to — `JsTransformOptions` is per-request context built by Metro, and `config.transformer.*` never flows into it. Result: every option passed to `withReactNativeCSS` (including `inlineRem`) was silently dropped before reaching `compile()`. Bug 2 — regex-discovered rem isn't propagated downstream -------------------------------------------------------- `compile()` contains a fallback that scans the CSS for `:root { font-size: Npx }` when `inlineRem` isn't explicitly set, so users can configure rem scaling directly from CSS. The discovered value was only wired into the first-pass lightningcss `Length` visitor via a local `effectiveRem`. It was never written back into `options`, so the second code path — `parseLength()` in `declarations.ts`, which reads `inlineRem` from `builder.getOptions()` — kept using the default 14. This is invisible on Tailwind v3, which emits `.text-base { font-size: 1rem }` directly: the `1rem` is a proper Length node, so the first-pass visitor handles it and scales correctly. It breaks on Tailwind v4, which emits `:root { --text-base: 1rem }` + `.text-base { font-size: var(--text-base) }`. The `1rem` inside the custom property is a token-list, inlined verbatim by `inlineVariables`, then resolved via `parseLength` in the second pass — where it reads the stale default. Net effect on v4: every `text-*` utility was sized against rem=14 regardless of what `:root { font-size }` declared or what the user configured, while direct `.my-class { font-size: 1.5rem }` rules scaled correctly. The divergence is why the bug evaded isolated tests. The fix ------- - metro-transformer.ts: read `reactNativeCSS` from `config`, not `options`. Drop the unreachable `options.reactNativeCSS` spread and its type intersection. - compiler.ts: after resolving `effectiveRem`, write it back to `options.inlineRem` so the builder's downstream consumers see the same value as the first-pass visitor. --- src/compiler/compiler.ts | 1 + src/metro/metro-transformer.ts | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 8245e64b..214cd615 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -92,6 +92,7 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) { const css = typeof code === "string" ? code : code.toString(); const match = css.match(/:root\s*\{[^}]*font-size:\s*([\d.]+)px/); effectiveRem = match?.[1] ? parseFloat(match[1]) : 14; + options.inlineRem = effectiveRem; } const firstPassVisitor: Visitor = {}; diff --git a/src/metro/metro-transformer.ts b/src/metro/metro-transformer.ts index e935ba67..e56e1ca7 100644 --- a/src/metro/metro-transformer.ts +++ b/src/metro/metro-transformer.ts @@ -13,13 +13,13 @@ const worker = require(unstable_transformerPath) as typeof import("metro-transform-worker"); export async function transform( - config: JsTransformerConfig, + config: JsTransformerConfig & { + reactNativeCSS?: CompilerOptions | undefined; + }, projectRoot: string, filePath: string, data: Buffer, - options: JsTransformOptions & { - reactNativeCSS?: CompilerOptions | undefined; - }, + options: JsTransformOptions, ): Promise { const isCss = options.type !== "asset" && /\.(s?css|sass)$/.test(filePath); @@ -37,7 +37,7 @@ export async function transform( const css = cssFile.output[0].data.css.code.toString(); const productionJS = compile(css, { - ...options.reactNativeCSS, + ...config.reactNativeCSS, filename: filePath, projectRoot: projectRoot, }).stylesheet(); From 46ef4c4d505381af2d4cabfbe805214ba9c509a2 Mon Sep 17 00:00:00 2001 From: Maxence Henneron Date: Mon, 20 Apr 2026 14:27:03 -0700 Subject: [PATCH 2/2] test: add regression test for Tailwind v4 rem propagation fix --- src/__tests__/native/units.test.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/__tests__/native/units.test.tsx b/src/__tests__/native/units.test.tsx index ebd9fc37..80950021 100644 --- a/src/__tests__/native/units.test.tsx +++ b/src/__tests__/native/units.test.tsx @@ -148,6 +148,23 @@ test("rem - css root font-size override", () => { }); }); +test("rem - via var() inlining picks up css root font-size ", () => { + registerCSS(` + :root { font-size: 16px; --text-base: 1rem; } + .text-base { font-size: var(--text-base); } + `); + + const { result } = renderHook(() => { + return useNativeCss(View, { className: "text-base" }); + }); + + expect(result.current.type).toBe(VariableContext.Provider); + expect(result.current.props.children.type).toBe(View); + expect(result.current.props.children.props).toStrictEqual({ + style: { fontSize: 16 }, + }); +}); + test("rem - css override", () => { registerCSS( `