What's Changed
A minor release with one breaking change: Unistyles now requires React Native 0.85+ (Expo SDK 56+). The experimental Metro integration no longer runs any Babel transform, and on web the stylesheet is updated rule by rule instead of being rewritten, so adding and removing styles is 2–8× faster and re-applying unchanged ones on resize is 100× faster or more. Fixes cover Android layouts stuck in the previous orientation after a rotation, useUnistyles after Fast Refresh, nested Text colors next to Reanimated views, setRootViewBackgroundColor while a Modal is open, RTL detection and the components/native/* exports.
🔀 Switching from the Babel plugin to Metro
The Babel plugin will be deprecated in the next release (see below). If your app bundles with Metro, you can switch now in three steps:
- Wrap your Metro config:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
// or for bare React Native
// const { getDefaultConfig } = require('@react-native/metro-config')
const { withUnistylesConfig } = require('react-native-unistyles/metro')
module.exports = withUnistylesConfig(getDefaultConfig(__dirname))- Remove the plugin from
babel.config.js. Unistyles throws an error if both are set up:
plugins: [
- ['react-native-unistyles/plugin', { root: 'src' }]
]- Use the styles that
useVariantsreturns. The codemod converts your components:
npx jscodeshift --parser=tsx --extensions=tsx,ts,jsx,js -t node_modules/react-native-unistyles/codemods/use-variants.js srcThen restart Metro with a clear cache (npx expo start -c or npx react-native start --reset-cache). Re.Pack, Vite, Next.js and other web bundlers keep using the Babel plugin, and Jest needs no changes. Full guide
⚠️ Breaking change
- The minimum React Native version goes from 0.81 to 0.85, and from Expo SDK 54 to 56 (the first SDK that ships 0.85). The
react-nativepeer dependency is now>=0.85.0, so your package manager warns before the native build fails. The New Architecture is still required (#1362)
📣 Upcoming deprecation
- The Babel plugin (
react-native-unistyles/plugin) will be deprecated in the next release and removed in Unistyles 4.0. Nothing changes in 3.8.0. If you bundle with Metro, you can switch now, see "Switching from the Babel plugin to Metro" above
🚇 Experimental Metro integration
withUnistylesConfigno longer runs the Babel plugin's transforms. Unistyles now records what each StyleSheet reads fromrtwhile it runs: through a HostObject on native and a Proxy on web. Dependencies read behind a condition, in a helper or in a dynamic function are picked up too. What a StyleSheet function reads counts for all of its styles, so Unistyles recomputes the whole StyleSheet and updates only the views whose styles changed. On the e2e runtime screen, an accent change now commits 3 views instead of 133. To keep a value that changes every frame (likert.insets.ime) to the views that use it, read it inside a dynamic function (#1355, docs)- If you already use the Metro integration:
styles.useVariants()no longer changesstyles. Use the styles it returns. This form works with the Babel plugin too, and a codemod converts your components:
// before
styles.useVariants({ size })
return <View style={styles.button} />
// 3.8.0
const variantStyles = styles.useVariants({ size })
return <View style={variantStyles.button} />npx jscodeshift --parser=tsx --extensions=tsx,ts,jsx,js -t node_modules/react-native-unistyles/codemods/use-variants.js src- The
debugoption is gone, andwithUnistylesConfignow wraps onlyresolveRequest, notbabelTransformerPath - Unistyles reached through a symlink is handled the same way. A second copy of Unistyles (e.g. nested in a library's
node_modules) now prints a Metro warning, because its styles won't update - With the Metro integration on native,
withUnistylescomponents re-render for other changes only when their computed styles changed. Theme changes re-render them as before (#1355)
⚡️ Web performance
- Web no longer rewrites the whole
<style id="unistyles-web">tag on every style add or remove. Unistyles now inserts, edits and deletes single rules through the CSSOM, skips rules that haven't changed, andupdateThemereplaces only that theme's rules (#1354 by @whydidoo)
| Chromium 3.7.0 | Chromium 3.8.0 | Safari 3.7.0 | Safari 3.8.0 | |
|---|---|---|---|---|
| Add 200 styles | 847 | 173 | 1783 | 867 |
| Remove 200 styles | 740 | 88 | 1674 | 679 |
| Re-apply 500 unchanged (resize) | 511 | 5 | 767 | 2 |
updateTheme ×20
| 110 | 69 | 181 | 91 |
ms, 3000 elements and 2000 styles, layout read after each operation
#unistyles-webno longer has text content. DevTools still shows its rules. If your code readsinnerTextortextContentfrom that tag, readsheet.cssRulesinstead
🎨 Themes and styles
- After a theme change, a
Textnested in anotherTextnext to a Reanimated animated view (e.g. a swipeable row) no longer keeps its old color. Reanimated clones React's nodes before they mount, and Unistyles used to skip them, so React's next commit brought back the old children (#1353, closes #1329) useUnistyles()andwithUnistyleskeep following theme changes after a Fast Refresh. Before, they stayed on the old theme until the app restarted (#1358, closes #1316)- Several
updateThemecalls in a row no longer let React re-commit views with styles from before the first update. When a later change committed nothing, Unistyles dropped the handles of the earlier commit. This affected the Babel plugin too (#1355) rt.rtlnow matches React Native'sI18nManager.isRTL. Before, Unistyles worked out RTL on its own and ignoredI18nManager.allowRTL(false), Android'ssupportsRtland iOS apps without an RTL localization, sort.rtlcould say RTL while React Native laid the screen out LTR. On Android,allowRTLandforceRTLchanges now show up at runtime (#1356)transformOriginstrings now accept signed and decimal values on native, e.g.'-12.5px 50.5%'(#1356)
🤖 Android
- After a rotation, the app is no longer sometimes laid out for the previous orientation until the next rotation. Unistyles committed style updates on a copy of the root taken before the commit, so when the new screen size arrived in between, it put the old size back. It now clones from the root it's committing to. In the reporter's repro, 3.6.0 got stuck in 3 of 40 rotations and 3.8.0 in none (#1357, closes #1347)
🍏 iOS
UnistylesRuntime.setRootViewBackgroundColorcalled while a React NativeModalis open now colors the app's root view. Before, it colored the Modal and the root view kept its old color (#1359, closes #1333)