Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .config/jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { setUpTests } from "react-native-reanimated";

/* global jest */

// Worklets 0.10 requires its native implementation to be mocked in Jest.
jest.mock("react-native-worklets", () =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Jest returns the untyped Worklets mock module.
jest.requireActual("react-native-worklets/src/mock"),
);

setUpTests();
23 changes: 20 additions & 3 deletions .github/actions/ios-dev-app/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ description: Create IOS Development App
runs:
using: composite
steps:
- name: Verify Xcode
shell: bash
run: xcodebuild -version

- name: Cache iOS development build
id: ios-dev-cache
uses: actions/cache@v4
with:
path: |
example/ios/build/DerivedData
key: ios-dev-${{ runner.os }}-${{ hashFiles('example/ios/**', 'example/package.json', 'package.json') }}
key: ios-dev-${{ runner.os }}-${{ runner.arch }}-xcode26.6-${{ hashFiles('example/ios/**', 'example/package.json', 'package.json', 'yarn.lock') }}
restore-keys: |
ios-dev-${{ runner.os }}-
ios-dev-${{ runner.os }}-${{ runner.arch }}-xcode26.6-

- name: Check for cached build
shell: bash
Expand Down Expand Up @@ -55,9 +59,11 @@ runs:
-scheme example \
-configuration Release \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build/DerivedData \
-resultBundlePath "$RUNNER_TEMP/ios-build.xcresult" \
CODE_SIGNING_ALLOWED=NO \
build | xcpretty
build 2>&1 | tee "$RUNNER_TEMP/ios-build.log" | xcpretty

cd ..

Expand All @@ -78,6 +84,17 @@ runs:
# Create a tar for better caching (go back to example directory)
tar -czf ios-dev-build.tar.gz -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"

- name: Upload iOS build diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: ios-build-diagnostics
path: |
${{ runner.temp }}/ios-build.log
${{ runner.temp }}/ios-build.xcresult
if-no-files-found: ignore
retention-days: 7

- name: Upload development build
if: steps.ios-dev-cache.outputs.cache-hit != 'true'
uses: actions/upload-artifact@v4
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
- name: Setup
uses: ./.github/actions/setup

- name: Build package
run: yarn build

- name: Lint files
run: yarn lint

Expand Down Expand Up @@ -53,6 +56,9 @@ jobs:
- name: Build package
run: yarn build

- name: Verify Node tooling
run: node --test node/__tests__/*.mjs

- name: Check for unstaged files
uses: ./.github/actions/check-unstaged-files

Expand All @@ -76,7 +82,9 @@ jobs:
uses: ./.github/actions/check-unstaged-files

build-ios-dev:
runs-on: macos-15
runs-on: macos-26
env:
DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer
steps:
- name: Checkout Repository
uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.css";
22 changes: 22 additions & 0 deletions docs/css-variable-inputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# CSS variable input units

Use explicit CSS lengths when a variable supplies a dimension in an application that also targets browsers:

```tsx
<VariableContextProvider value={{ "--card-width": "80.5px", "--opacity": 0.5 }}>
<View className="card" />
</VariableContextProvider>
```

```css
.card {
width: var(--card-width);
opacity: var(--opacity);
}
```

The native engine converts pixel strings to React Native numeric dimensions. Fractional values must remain fractional when the variable is inherited or updated. The proposed Expo upgrade fixes the previous truncation of `80.5px` to `80`.

Browser custom properties retain their CSS token values. A nonzero unitless number is invalid when substituted directly into `width`, while a unitless opacity or scale factor is valid. The library cannot append `px` to every numeric variable without changing those other uses. Use a pixel string for lengths.

`vars()` remains a deprecated input path; the same unit contract applies. Prefer `VariableContextProvider` for new code. Source tests cover fractional pixel values through both APIs and their updates. Compatibility evidence keeps the original numeric width case distinct from the explicit length case rather than changing the original expectation.
21 changes: 21 additions & 0 deletions docs/expo57-rc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# react-native-css Expo 57 release candidate

Publication draft. The proposed engine is react-native-css 3.1.0-rc.0, paired with Nativewind 5.0.0-rc.0. The packages are not published yet. Nativewind requires this exact engine candidate.

The audited target is Expo 57.0.22, React Native 0.86.3, React 19.2.3, Reanimated 4.5.1 and Worklets 0.10.1. Keep native dependency versions aligned with Expo. Native dependency changes require rebuilding the application.

After publication, install the exact engine and Nativewind pair:

```sh
npm install --save-exact nativewind@5.0.0-rc.0 react-native-css@3.1.0-rc.0
```

The engine source used to prepare the archive is commit 06b7bda3cc04b76c62715e537c371ad16869ec8b. The archive SHA256 is a03235e206ef49e48fb960e59eebea14bbb69e623aa168250d8ccca2ff200072. This documentation commit does not change the audited package contents.

The RC updates Expo integration, Node ESM tooling exports, TypeScript setup, style and prop mappings, variable resolution, compiler behavior, component identities and cache invalidation. Each accepted contract is associated with explicit expected values and deliberately incorrect controls in the compatibility audit. The full library run passed 1434 tests across the two libraries plus 42 engine Node tests. The release verifier passed 670 tests with no skips. The reviewed inventory accounts for 6129 entries and requires 4985 execution cells. All 4985 passed the final release checker, with zero missing assertions. Public source review and a subsequent publication instruction remain necessary before npm release.

Android animation cancellation remains affected by [Reanimated issue 10507](https://github.com/software-mansion/react-native-reanimated/issues/10507), which also reproduces without react-native-css or Nativewind. The exact Android animate-none reset case is retained as an accepted upstream defect and excluded from passing support claims. Its isolated result can pass, so the cancellation behavior remains intermittent. iPhone and browser cancellation and every other motion requirement remain independently verified. No Reanimated patch is bundled. Physical Android verification was waived; a Release emulator and physical iPhone are required. Browser object fitting in the original React Native Web and Expo Image adapters requires explicit component props. Some browser selection, backface and fragmentation examples remain explicitly limited. These are not universal CSS support claims.

Use React Native Appearance and useColorScheme for native dark mode. Appearance.setColorScheme('unspecified') restores the system preference on this Expo target. The full [Nativewind RC compatibility guide](https://github.com/nativewind/nativewind/blob/danstepanov/expo-57-rc-review/docs/rc-compatibility.md) records supported value domains, migration requirements and exact platform limitations. The [installation guide](https://github.com/nativewind/nativewind/blob/danstepanov/expo-57-rc-review/docs/expo57-rc.md) includes Tailwind, PostCSS, Metro and Babel configuration.

Report problems with exact package and Expo versions, platform, build mode, configuration and a minimal reproduction. Include whether a direct React Native or Reanimated reference also fails. The public v4 to v5 migration skill follows RC publication and must be verified against the published packages before stable promotion. Stable npm tags stay unchanged during the RC release.
9 changes: 9 additions & 0 deletions docs/known-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Known Expo 57 dependency limitation

With Expo 57.0.22, React Native 0.86.3, Reanimated 4.5.1, and Worklets 0.10.1, cancelling a CSS animation on Android can leave the component at its last animated transform. A spinning view can remain tilted after switching to `animationName: "none"` or removing the animation styles. In Nativewind, this affects changing `animate-spin` to `animate-none`.

The original Expo 57.0.21 diagnostic reproduced both forms. Fresh Expo 57.0.22 checks reproduce direct style removal and integrated animate-none failure, while isolated none checks can pass. The behavior is intermittent. The failure reproduces with a direct Reanimated `Animated.View` without Nativewind or react-native-css. It is tracked in [Reanimated #10507](https://github.com/software-mansion/react-native-reanimated/issues/10507). The confirmed environment is an Android API 34 emulator with Fabric, Hermes, and a Release build. The integrated physical iPhone cancellation case passed. Later Reanimated versions and physical Android have not been verified.

The planned RC retains Expo's exact dependency versions and discloses this limitation. Neither library includes the experimental Reanimated patch. Applications relying on CSS animation cancellation must account for this known behavior. No production workaround is currently verified by this release effort.

The issue includes a [standalone reproduction](https://gist.github.com/danstepanov/03d34ece59f03628deb77a028e8a9a03). When an official fix becomes available in the supported Expo environment, rerun the cancellation and motion checks before removing this notice. The full RC gate is complete as described in [the release guide](expo57-rc.md). The packages have not been published.
11 changes: 11 additions & 0 deletions docs/v5-engine-contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# V5 engine contract migration notes

Nativewind v5 dark mode follows the [documented Appearance API](https://www.nativewind.dev/v5/core-concepts/dark-mode). The default `dark:` variant compiles to `prefers-color-scheme: dark`. On native, use React Native `Appearance.setColorScheme("light")` or `Appearance.setColorScheme("dark")` for manual selection, and `useColorScheme` from `react-native` to read it. On this Expo target, `Appearance.setColorScheme("unspecified")` restores the system preference. Web uses its CSS media query; native override verification does not establish a browser override mechanism.

Legacy `@cssInterop set darkMode ...` configuration and class-qualified `:root` selectors have no native document root contract. The compiler now reports an explicit error before variable optimization can accidentally apply conditional values unconditionally. Migrate theme behavior to media queries and Appearance. Ordinary ancestor class selectors remain supported as selectors; adding a `dark` ancestor is not required by the documented default v5 theme API.

For preserved CSS variables, use the compiler option `inlineVariables: { exclude: ["--variable-name"] }`. The old `@react-native config { preserve-variables: ... }` directive reports a migration error rather than silently ignoring the option.

The declared deprecated `nativeStyleToProp` option remains supported as an alias for `nativeStyleMapping`. The current option takes precedence when both are provided, including an empty mapping. `target: false` discards unmapped compiled styles while retaining original inline styles.

Unit verification covers compiler semantics, Appearance event subscription, mapping destinations and restoration, and animation metadata delivered to Reanimated. It does not prove native frame interpolation or OS event delivery. The new engine package still requires renderer verification before RC approval.
58 changes: 1 addition & 57 deletions example/example-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1 @@
// This file is should be auto generated, you do not need to use this file

export * from "react-native";

declare module "react-native" {
interface ScrollViewProps
extends ViewProps,
ScrollViewPropsIOS,
ScrollViewPropsAndroid,
Touchable {
contentContainerClassName?: string;
indicatorClassName?: string;
}
interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
columnWrapperClassName?: string;
}
interface ImageBackgroundProps extends ImagePropsBase {
imageClassName?: string;
}
interface ImagePropsBase {
className?: string;
cssInterop?: boolean;
}
interface ViewProps {
className?: string;
cssInterop?: boolean;
}
interface TextInputProps {
placeholderClassName?: string;
}
interface TextProps {
className?: string;
cssInterop?: boolean;
}
interface SwitchProps {
className?: string;
cssInterop?: boolean;
}
interface InputAccessoryViewProps {
className?: string;
cssInterop?: boolean;
}
interface TouchableWithoutFeedbackProps {
className?: string;
cssInterop?: boolean;
}
interface StatusBarProps {
className?: string;
cssInterop?: boolean;
}
interface KeyboardAvoidingViewProps extends ViewProps {
contentContainerClassName?: string;
}
interface ModalBaseProps {
presentationClassName?: string;
}
}
/// <reference types="react-native-css/types" />
22 changes: 13 additions & 9 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~6.1.2",
"@babel/core": "^7.29.0",
"@expo/metro-config": "57.0.12",
"@expo/metro-runtime": "~57.0.15",
"@react-native/metro-config": "0.86.3",
"@tailwindcss/postcss": "^4.1.11",
"expo": "54.0.10",
"expo-status-bar": "~3.0.8",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"expo": "57.0.22",
"expo-status-bar": "~57.0.1",
"lightningcss": "^1.30.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.86.3",
"react-native-css": "link:../",
"react-native-reanimated": "~4.1.0",
"react-native-web": "~0.21.1",
"react-native-worklets": "~0.5.0"
"react-native-reanimated": "4.5.1",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.10.1"
},
"private": true
}
99 changes: 99 additions & 0 deletions node/__tests__/cache-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import assert from "node:assert/strict";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
rmSync,
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";

const require = createRequire(import.meta.url);
const helper = require.resolve("../../dist/commonjs/metro/cache-version.js");

/** @param {import("node:test").TestContext} t */
function fixture(t) {
const root = mkdtempSync(join(tmpdir(), "css-cache-version-"));
t.after(() => {
rmSync(root, { recursive: true, force: true });
});
mkdirSync(join(root, "metro"));
mkdirSync(join(root, "compiler"));
copyFileSync(helper, join(root, "metro/cache-version.cjs"));
writeFileSync(join(root, "compiler/value.js"), "exports.value = 14;");
return {
root,
key: /** @type {{getCacheVersion: (version?: string, options?: object) => string}} */ (
require(join(root, "metro/cache-version.cjs"))
).getCacheVersion,
};
}

await test("unchanged input and relocated packages have the same fingerprint", (t) => {
const a = fixture(t),
b = fixture(t);
assert.equal(a.key("user", {}), a.key("user", {}));
assert.equal(a.key("user", {}), b.key("user", {}));
});

await test("compiler code changes invalidate and restoration recovers the key", (t) => {
const { root, key } = fixture(t);
const before = key("user", {});
writeFileSync(join(root, "compiler/value.js"), "exports.value = 18;");
assert.notEqual(key("user", {}), before);
writeFileSync(join(root, "compiler/value.js"), "exports.value = 14;");
assert.equal(key("user", {}), before);
});

await test("new source modules invalidate and removal recovers the key", (t) => {
const { root, key } = fixture(t);
const before = key("user", {});
writeFileSync(join(root, "compiler/new.js"), "exports.value = 18;");
assert.notEqual(key("user", {}), before);
rmSync(join(root, "compiler/new.js"));
assert.equal(key("user", {}), before);
});

await test("compiler options invalidate the cache", (t) => {
const { key } = fixture(t);
assert.notEqual(
key("user", { inlineRem: 14 }),
key("user", { inlineRem: 18 }),
);
assert.notEqual(key("user", { inlineVariables: false }), key("user", {}));
assert.notEqual(key("user", { features: { test: true } }), key("user", {}));
});

await test("user cacheVersion is preserved and remains an invalidation input", (t) => {
const { key } = fixture(t);
assert.match(key("user", {}), /^user:react-native-css:[0-9a-f]{64}$/);
assert.notEqual(key("user", {}), key("other", {}));
assert.equal(key(undefined, undefined), key("", {}));
});

await test("maps, declarations, tests, and unrelated files do not invalidate", (t) => {
const { root, key } = fixture(t);
const before = key("user", {});
mkdirSync(join(root, "compiler/__tests__"));
for (const name of [
"compiler/value.js.map",
"compiler/value.d.ts",
"compiler/__tests__/value.js",
"compiler/value.test.js",
"compiler/value.spec.ts",
"README.md",
]) {
writeFileSync(join(root, name), "ignored");
}
assert.equal(key("user", {}), before);
});

await test("source condition TypeScript modules invalidate", (t) => {
const { root, key } = fixture(t);
const before = key("user", {});
writeFileSync(join(root, "compiler/source.ts"), "export const value = 18;");
assert.notEqual(key("user", {}), before);
});
Loading
Loading