migrate-nativewind-to-uniwind by uni-stack/uniwind
npx skills add https://github.com/uni-stack/uniwind --skill migrate-nativewind-to-uniwindUniwind 以更好的性能和稳定性取代了 NativeWind。它需要 Tailwind CSS 4 并使用基于 CSS 的主题化,而非 JS 配置。
开始之前,请阅读项目的现有配置文件以了解当前设置:
package.json (NativeWind 版本、依赖项)tailwind.config.js / tailwind.config.tsmetro.config.jsbabel.config.jsglobal.css 或等效的 CSS 入口文件nativewind-env.d.ts 或 nativewind.d.ts广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
nativewind 的 cssInterop 或 remapProps 的文件react-native-css-interop 导入的文件vars() 用法)卸载所有这些包(如果存在):
npm uninstall nativewind react-native-css-interop
# 或
yarn remove nativewind react-native-css-interop
# 或
bun remove nativewind react-native-css-interop
关键:react-native-css-interop 是 NativeWind 的依赖项,必须移除。在迁移过程中常被遗漏。在整个代码库中搜索任何来自它的导入:
rg "react-native-css-interop" -g "*.{ts,tsx,js,jsx}"
移除找到的每一个导入和使用。
npm install uniwind tailwindcss@latest
# 或
yarn add uniwind tailwindcss@latest
# 或
bun add uniwind tailwindcss@latest
确保 tailwindcss 版本为 4+。
移除 NativeWind 的 babel 预设:
// 从 presets 数组中移除此行:
// 'nativewind/babel'
不需要 Uniwind 的 babel 预设。
将 NativeWind 的 metro 配置替换为 Uniwind 的。withUniwindConfig 必须是 最外层的包装器。
之前 (NativeWind):
const { withNativeWind } = require('nativewind/metro');
module.exports = withNativeWind(config, { input: './global.css' });
之后 (Uniwind):
const { getDefaultConfig } = require('expo/metro-config');
// 对于 bare RN: const { getDefaultConfig } = require('@react-native/metro-config');
const { withUniwindConfig } = require('uniwind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
polyfills: { rem: 14 },
});
cssEntryFile 必须是从项目根目录出发的 相对路径字符串(例如 ./global.css 或 ./app/global.css)。对于此选项,不要使用绝对路径或 path.resolve(...) / path.join(...)。
// ❌ 错误
cssEntryFile: path.resolve(__dirname, 'app', 'global.css')
// ✅ 正确
cssEntryFile: './app/global.css'
始终将 polyfills.rem 设置为 14 以匹配 NativeWind 的默认 rem 值,并防止迁移后间距/尺寸出现差异。
如果项目使用了 light/dark 之外的自定义主题(例如通过 NativeWind 的 vars() 或自定义 ThemeProvider 定义),请使用 extraThemes 注册它们。不要包含 light 或 dark —— 它们会自动添加:
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
polyfills: { rem: 14 },
extraThemes: ['ocean', 'sunset', 'premium'],
});
选项:
cssEntryFile (必需):CSS 入口文件的相对路径字符串(从项目根目录出发)polyfills.rem (迁移必需):设置为 14 以匹配 NativeWind 的 rem 基准extraThemes (如果项目有自定义主题则必需):自定义主题名称数组 —— 不要包含 light/darkdtsFile (可选):生成的 TypeScript 类型文件的路径,默认为 ./uniwind-types.d.tsdebug (可选):在开发期间记录不受支持的 CSS 属性将 NativeWind 的 Tailwind 3 指令替换为 Tailwind 4 导入:
之前:
@tailwind base;
@tailwind components;
@tailwind utilities;
之后:
@import 'tailwindcss';
@import 'uniwind';
确保 global.css 在你的主 App 组件中导入(例如 App.tsx),不要在注册应用的根 index.ts/index.js 中导入 —— 在那里导入会破坏热重载。
删除 nativewind-env.d.ts 或 nativewind.d.ts。Uniwind 会在 dtsFile 指定的路径自动生成自己的类型。
完全移除 tailwind.config.js / tailwind.config.ts。所有主题配置都使用 Tailwind 4 的 @theme 指令迁移到 CSS。
将自定义主题值迁移到 global.css:
之前 (tailwind.config.js):
module.exports = {
theme: {
extend: {
colors: {
primary: '#00a8ff',
secondary: '#273c75',
},
fontFamily: {
normal: ['Roboto-Regular'],
bold: ['Roboto-Bold'],
},
},
},
};
之后 (global.css):
@import 'tailwindcss';
@import 'uniwind';
@theme {
--color-primary: #00a8ff;
--color-secondary: #273c75;
--font-normal: 'Roboto-Regular';
--font-bold: 'Roboto-Bold';
}
字体系列必须指定 单一字体 —— React Native 不支持字体回退。
这是最常被遗漏的步骤。 搜索整个代码库:
rg "cssInterop|remapProps" -g "*.{ts,tsx,js,jsx}"
将每个 cssInterop() / remapProps() 调用替换为 Uniwind 的 withUniwind():
之前 (NativeWind):
import { cssInterop } from 'react-native-css-interop';
import { Image } from 'expo-image';
cssInterop(Image, { className: 'style' });
之后 (Uniwind):
import { withUniwind } from 'uniwind';
import { Image as ExpoImage } from 'expo-image';
export const Image = withUniwind(ExpoImage);
withUniwind 会自动映射 className → style 和其他常见属性。对于自定义属性映射:
const StyledProgressBar = withUniwind(ProgressBar, {
width: {
fromClassName: 'widthClassName',
styleProperty: 'width',
},
});
在 模块级别 定义包装组件(不要在渲染函数内部)。每个组件只应包装一次:
仅在一个文件中使用 —— 在同一文件中定义包装组件:
// screens/ProfileScreen.tsx
import { withUniwind } from 'uniwind';
import { BlurView as RNBlurView } from '@react-native-community/blur';
const BlurView = withUniwind(RNBlurView);
export function ProfileScreen() {
return <BlurView className="flex-1" />;
}
在多个文件中使用 —— 在共享模块中包装一次并重新导出:
// components/styled.ts
import { withUniwind } from 'uniwind';
import { Image as ExpoImage } from 'expo-image';
import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
export const Image = withUniwind(ExpoImage);
export const LinearGradient = withUniwind(RNLinearGradient);
然后从共享模块中随处导入:
```javascript
import { Image, LinearGradient } from '@/components/styled';
```
切勿在多个文件中为同一组件调用 withUniwind —— 包装一次,随处导入。
重要:不要用 withUniwind 包装来自 react-native 或 react-native-reanimated 的组件 —— 它们已经开箱即用地支持 className。这包括 View、Text、Image、ScrollView、FlatList、Pressable、TextInput、Animated.View 等。仅对 第三方 组件使用 withUniwind(例如 expo-image、expo-linear-gradient、@react-native-community/blur)。
重要 —— 非样式颜色属性的 accent- 前缀:React Native 组件具有像 color、tintColor、backgroundColor 这样的属性,它们不属于 style 对象。要通过 Tailwind 类设置这些属性,请使用 accent- 前缀和相应的 *ClassName 属性:
// color 属性 → 带有 accent- 前缀的 colorClassName
<ActivityIndicator
className="m-4"
size="large"
colorClassName="accent-blue-500 dark:accent-blue-400"
/>
// Button 上的 color 属性
<Button
colorClassName="accent-background"
title="Press me"
/>
// tintColor 属性 → 带有 accent- 前缀的 tintColorClassName
<Image
className="w-6 h-6"
tintColorClassName="accent-red-500"
source={icon}
/>
规则:className 接受任何用于基于样式的属性的 Tailwind 工具类。对于非样式属性(color、tintColor 等),请使用带有 accent- 前缀的 {propName}ClassName。这适用于所有内置的 React Native 组件。
之前 (使用 vars() 的 NativeWind JS 主题):
import { vars } from 'nativewind';
export const themes = {
light: vars({
'--color-primary': '#00a8ff',
'--color-typography': '#000',
}),
dark: vars({
'--color-primary': '#273c75',
'--color-typography': '#fff',
}),
};
// 在 JSX 中:
<View style={themes[colorScheme]}>
之后 (Uniwind CSS 主题):
@layer theme {
:root {
@variant light {
--color-primary: #00a8ff;
--color-typography: #000;
}
@variant dark {
--color-primary: #273c75;
--color-typography: #fff;
}
}
}
重要:所有主题变体必须定义完全相同的 CSS 变量集合。如果 light 定义了 --color-primary 和 --color-typography,那么 dark(以及任何自定义主题)也必须定义两者。不匹配的变量将导致 Uniwind 运行时错误。
不需要 ThemeProvider 包装器。从 JSX 中移除 NativeWind 的 <ThemeProvider> 或 vars() 包装器。如果使用了 React Navigation 的 <ThemeProvider>,请保留。
如果项目使用了嵌套的主题包装器来预览或强制为特定子树应用主题(例如演示卡片、设置预览或并排主题比较),请使用 Uniwind Pro 的 ScopedTheme,而不是更改全局主题:
import { ScopedTheme } from 'uniwind';
<ScopedTheme theme="dark">
<PreviewCard />
</ScopedTheme>
如果项目有 light/dark 之外的自定义主题(例如 ocean、premium),你必须:
在 CSS 中使用 @variant 定义它们:
@layer theme {
:root {
@variant ocean {
--color-primary: #0ea5e9;
--color-background: #0c4a6e;
}
}
}
通过 extraThemes 在 metro.config.js 中注册它们(跳过 light/dark —— 它们会自动添加):
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
polyfills: { rem: 14 },
extraThemes: ['ocean', 'premium'],
});
NativeWind 的安全区域类需要在 Uniwind 中显式设置:
import { SafeAreaProvider, SafeAreaListener } from 'react-native-safe-area-context';
import { Uniwind } from 'uniwind';
export default function App() {
return (
<SafeAreaProvider>
<SafeAreaListener
onChange={({ insets }) => {
Uniwind.updateInsets(insets);
}}
>
<View className="pt-safe px-safe">
{/* content */}
</View>
</SafeAreaListener>
</SafeAreaProvider>
);
}
NativeWind 使用 14px 作为基准 rem,Uniwind 默认为 16px。步骤 4 已经在 metro 配置中设置了 polyfills: { rem: 14 } 以保留 NativeWind 的间距。如果用户明确想要 Uniwind 的默认值(16px),他们可以移除 polyfill —— 但要警告他们所有间距/尺寸都会改变。
Uniwind 不会自动去重冲突的 className(NativeWind 会)。如果你的代码库依赖于像 className={p-4 ${overrideClass}} 这样的覆盖模式,请设置一个 cn 工具函数。
首先,检查项目是否已经有 cn 辅助函数(在 shadcn/ui 项目中常见):
rg "export function cn|export const cn" -g "*.{ts,tsx,js}"
如果存在,保持原样。如果不存在,安装依赖项并创建它:
npm install tailwind-merge clsx
创建 lib/cn.ts(或项目中工具函数存放的位置):
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
用法:
import { cn } from '@/lib/cn';
<View className={cn('p-4 bg-white', props.className)} />
<Text className={cn('text-base', isActive && 'text-blue-500', disabled && 'opacity-50')} />
使用 cn 而不是原始的 twMerge —— 它通过 clsx 处理条件类、数组和假值,然后再用 tailwind-merge 去重。
如果项目使用了 NativeWind 的 animated-* / 过渡类模式,请将这些迁移到显式的 react-native-reanimated 用法。Uniwind OSS 不提供 NativeWind 风格的动画类行为。
请将此迁移指南部分作为权威来源:
最终扫描 —— 搜索并移除任何剩余的引用:
rg "nativewind|NativeWind|native-wind" -g "*.{ts,tsx,js,jsx,json,css}"
检查:
package.json 中的 nativewind(包括 devDependencies)package.json 中的 react-native-css-interopbabel.config.js 中的 NativeWind babel 预设metro.config.js 中的 NativeWind metro 包装器nativewind-env.d.ts 或 nativewind.d.ts 文件cssInterop() 或 remapProps() 调用nativewind 导入的 vars()import { useUniwind } from 'uniwind';
const { theme, hasAdaptiveThemes } = useUniwind();
// theme: 当前主题名称 —— "light"、"dark"、"system" 或自定义主题
// hasAdaptiveThemes: 如果应用遵循系统配色方案则为 true
用于:在 UI 中显示主题名称、按主题进行条件渲染、主题变化时的副作用。
访问主题信息而不引起重新渲染:
import { Uniwind } from 'uniwind';
Uniwind.currentTheme // "light"、"dark"、"system" 或自定义主题
Uniwind.hasAdaptiveThemes // 如果遵循系统配色方案则为 true
用于:日志记录、分析、渲染之外的命令式逻辑。
将 Tailwind 类转换为 React Native 样式对象。用于处理不支持 className 且无法用 withUniwind 包装的组件(例如 react-navigation 主题配置):
import { useResolveClassNames } from 'uniwind';
const headerStyle = useResolveClassNames('bg-blue-500');
const cardStyle = useResolveClassNames('bg-white dark:bg-gray-900');
<Stack.Navigator
screenOptions={{
headerStyle: headerStyle,
cardStyle: cardStyle,
}}
/>
以编程方式检索 CSS 变量值。变量必须以 -- 为前缀,并且与 global.css 中定义的变量匹配:
import { useCSSVariable } from 'uniwind';
const primaryColor = useCSSVariable('--color-primary');
const spacing = useCSSVariable('--spacing-4');
用于:动画、第三方库配置、使用设计令牌进行计算。
使用设备感知的 CSS 函数(如 hairlineWidth()、fontScale()、pixelRatio())定义自定义工具类。这些可以在任何地方使用(自定义 CSS 类、@utility 等)—— 但不能在 @theme {} 内部使用(它只接受静态值)。使用 @utility 创建可重用的 Tailwind 风格类:
@utility w-hairline { width: hairlineWidth(); }
@utility h-hairline { height: hairlineWidth(); }
@utility border-hairline { border-width: hairlineWidth(); }
@utility text-scaled { font-size: fontScale(); }
然后使用:<View className="w-hairline h-hairline" />
使用 ios:、android:、web:、native: 前缀按平台有条件地应用样式:
<View className="ios:bg-red-500 android:bg-blue-500 web:bg-green-500">
<Text className="ios:text-white android:text-white web:text-black">
Platform-specific styles
</Text>
</View>
默认情况下,Uniwind 遵循系统配色方案(自适应主题)。要以编程方式切换主题:
import { Uniwind } from 'uniwind';
Uniwind.setTheme('dark'); // 强制深色
Uniwind.setTheme('light'); // 强制浅色
Uniwind.setTheme('system'); // 遵循系统(默认)
Uniwind.setTheme('ocean'); // 自定义主题(必须在 extraThemes 中)
当项目仅需要为 UI 的一部分(组件预览、主题化部分、嵌套演示)使用不同主题,而不更改整个应用的主题时,请使用 ScopedTheme:
import { ScopedTheme } from 'uniwind';
<View className="gap-3">
<PreviewCard />
<ScopedTheme theme="light">
<PreviewCard />
</ScopedTheme>
<ScopedTheme theme="dark">
<PreviewCard />
</ScopedTheme>
</View>
重要行为:
ScopedTheme 生效(支持嵌套作用域)useUniwind、useResolveClassNames 和 useCSSVariable 这样的钩子会针对最近的作用域主题进行解析withUniwind 包装的第三方组件也会从该作用域解析主题化值ScopedTheme 中使用自定义主题名称(必须在 extraThemes 中定义)首选使用基于 CSS 变量的类,而不是显式的 dark:/light: 变体。 而不是:
// 避免这种模式
<View className="light:bg-white dark:bg-black" />
定义一个 CSS 变量并直接使用它:
@layer theme {
:root {
@variant light { --color-background: #ffffff; }
@variant dark { --color-background: #000000; }
}
}
// 首选 —— 自动适应主题
<View className="bg-background" />
这样更简洁、更易于维护,并且也能自动适用于自定义主题。
在运行时更新主题变量,例如基于用户偏好或 API 响应:
import { Uniwind } from 'uniwind';
// 基于用户输入或 API 响应预配置主题
Uniwind.updateCSSVariables('light', {
'--color-primary': '#ff6600',
'--color-background': '#1a1a2e',
});
这种模式应仅用于应用有真正的运行时主题化需求时(例如,用户选择的品牌颜色或 API 驱动的主题)。
对于组件变体和复合变体,请使用 tailwind-variants 库:
import { tv } from 'tailwind-variants';
const button = tv({
base: 'px-4 py-2 rounded-lg',
variants: {
color: {
primary: 'bg-primary text-white',
secondary: 'bg-secondary text-white',
},
size: {
sm: 'text-sm',
lg: 'text-lg px-6 py-3',
},
},
});
<Pressable className={button({ color: 'primary', size: 'lg' })} />
如果项目是 monorepo,请在 global.css 中添加 @source 指令,以便 Tailwind 扫描 CSS 入口文件目录之外的包(仅当该目录包含带有 Tailwind 类的组件时):
@import 'tailwindcss';
@import 'uniwind';
@source "../../packages/ui/src";
@source "../../packages/shared/src";
自定义字体:Uniwind 仅将 className 映射到 font-family —— 字体文件必须单独加载(app.json 中的 expo-font 插件或用于 bare RN 的 react-native-asset)。@theme 中的字体系列名称必须与文件名完全匹配(不带扩展名)。使用 @variant 实现每平台字体(必须在 @layer theme { :root { } } 内部):
@layer theme {
:root {
@variant ios { --font-sans: 'SF Pro Text'; }
@variant android { --font-sans: 'Roboto-Regular'; }
@variant web { --font-sans: 'system-ui'; }
}
}
数据选择器:使用 data-[prop=value]:utility 进行基于属性的样式设置。仅支持相等性检查:
<View data-state={isOpen ? 'open' : 'closed'} className="data-[state=open]:bg-muted/50" />
Expo Router 中的 global.css 位置:放置在项目根目录并在根布局(app/_layout.tsx)中导入。如果放置在 app/ 中,外部组件需要 @source 指令。Tailwind 从 global.css 的位置开始扫描。
CSS 更改时完全重新加载应用:Metro 无法热重载包含许多提供者的文件。将 global.css 导入移动到组件树更深层(例如导航根或主屏幕)以修复。
渐变:内置支持,无需额外依赖。使用 bg-gradient-to-r from-red-500 via-yellow-500 to-green-500。对于 expo-linear-gradient,使用 useCSSVariable 获取颜色 —— withUniwind 将不起作用,因为渐变属性是数组。
样式特异性:内联 style 总是覆盖 className。使用 className 处理静态样式,仅对真正动态的值使用内联。避免对同一属性同时使用两者。
序列化错误 (Failed to serialize javascript object):清除缓存:watchman watch-del-all 2>/dev/null; rm -rf node_modules/.cache && npx expo start --clear。常见原因:复杂的 @theme 配置、循环 CSS 变量引用。
Metro unstable_enablePackageExports 冲突:某些应用(加密等)禁用了此功能,破坏了 Uniwind。使用选择性解析器:
config.resolver.unstable_enablePackageExports = false;
config.resolver.resolveRequest = (context, moduleName, platform) => {
if (['uniwind', 'culori'].some((prefix) => moduleName.startsWith(prefix))) {
return context.resolveRequest({ ...context, unstable_enablePackageExports: true }, moduleName, platform);
}
return context.resolveRequest(context, moduleName, platform);
};
安全区域类:p-safe、pt-safe、pb-safe、px-safe、py-safe、m-safe、mt-safe 等。还支持 -or-{value}(最小间距)和 -offset-{value}(额外间距)变体。
Next.js:官方不支持。Uniwind 适用于 Metro 和 Vite。社区插件:uniwind-plugin-next。对于 Next.js,请使用标准的 Tailwind CSS 并共享设计令牌。
Vite:自 v1.2.0 起支持。将 uniwind/vite 插件与 @tailwindcss/vite 一起使用。
UI 套件:HeroUI Native、react-native-reusables 和 Gluestack 4.1+ 与 Uniwind 配合良好。
data-[prop=value]:utility 语法,类似于 NativeWind。react-native-reanimated。Uniwind Pro 内置了 Reanimated 支持。迁移后,请验证:
npx react-native start --reset-cache(清除 Metro 缓存)或使用 expo npx expo start -cnativewind 或 react-native-css-interop 的导入重要:不要猜测 Uniwind API。如果你对任何 Uniwind API、钩子、组件或配置选项不确定,请获取并对照官方文档进行验证:https://docs.uniwind.dev/llms-full.txt
每周安装量
656
仓库
GitHub 星标
1.4K
首次出现
2026年2月12日
安全审计
安装于
codex646
opencode642
gemini-cli640
github-copilot639
amp631
kimi-cli630
Uniwind replaces NativeWind with better performance and stability. It requires Tailwind CSS 4 and uses CSS-based theming instead of JS config.
Before starting, read the project's existing config files to understand the current setup:
package.json (NativeWind version, dependencies)tailwind.config.js / tailwind.config.tsmetro.config.jsbabel.config.jsglobal.css or equivalent CSS entry filenativewind-env.d.ts or nativewind.d.tscssInterop or remapProps from nativewindreact-native-css-interopvars() usage)Uninstall ALL of these packages (if present):
npm uninstall nativewind react-native-css-interop
# or
yarn remove nativewind react-native-css-interop
# or
bun remove nativewind react-native-css-interop
CRITICAL : react-native-css-interop is a NativeWind dependency that must be removed. It is commonly missed during migration. Search the entire codebase for any imports from it:
rg "react-native-css-interop" -g "*.{ts,tsx,js,jsx}"
Remove every import and usage found.
npm install uniwind tailwindcss@latest
# or
yarn add uniwind tailwindcss@latest
# or
bun add uniwind tailwindcss@latest
Ensure tailwindcss is version 4+.
Remove the NativeWind babel preset:
// REMOVE this line from presets array:
// 'nativewind/babel'
No Uniwind babel preset is needed.
Replace NativeWind's metro config with Uniwind's. withUniwindConfig must be the outermost wrapper.
Before (NativeWind):
const { withNativeWind } = require('nativewind/metro');
module.exports = withNativeWind(config, { input: './global.css' });
After (Uniwind):
const { getDefaultConfig } = require('expo/metro-config');
// For bare RN: const { getDefaultConfig } = require('@react-native/metro-config');
const { withUniwindConfig } = require('uniwind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
polyfills: { rem: 14 },
});
cssEntryFile must be a relative path string from project root (e.g. ./global.css or ./app/global.css). Do not use absolute paths or path.resolve(...) / path.join(...) for this option.
// ❌ Broken
cssEntryFile: path.resolve(__dirname, 'app', 'global.css')
// ✅ Correct
cssEntryFile: './app/global.css'
Always setpolyfills.rem to 14 to match NativeWind's default rem value and prevent spacing/sizing differences after migration.
If the project uses custom themes beyond light/dark (e.g. defined via NativeWind's vars() or a custom ThemeProvider), register them with extraThemes. Do NOT include light or dark — they are added automatically:
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
polyfills: { rem: 14 },
extraThemes: ['ocean', 'sunset', 'premium'],
});
Options:
cssEntryFile (required): relative path string to CSS entry file (from project root)polyfills.rem (required for migration): set to 14 to match NativeWind's rem baseextraThemes (required if project has custom themes): array of custom theme names — do NOT include light/darkdtsFile (optional): path for generated TypeScript types, defaults to ./uniwind-types.d.tsdebug (optional): log unsupported CSS properties during devReplace NativeWind's Tailwind 3 directives with Tailwind 4 imports:
Before:
@tailwind base;
@tailwind components;
@tailwind utilities;
After:
@import 'tailwindcss';
@import 'uniwind';
Ensure global.css is imported in your main App component (e.g., App.tsx), NOT in the root index.ts/index.js where you register the app — importing there breaks hot reload.
Delete nativewind-env.d.ts or nativewind.d.ts. Uniwind auto-generates its own types at the path specified by dtsFile.
Remove tailwind.config.js / tailwind.config.ts entirely. All theme config moves to CSS using Tailwind 4's @theme directive.
Migrate custom theme values to global.css:
Before (tailwind.config.js):
module.exports = {
theme: {
extend: {
colors: {
primary: '#00a8ff',
secondary: '#273c75',
},
fontFamily: {
normal: ['Roboto-Regular'],
bold: ['Roboto-Bold'],
},
},
},
};
After (global.css):
@import 'tailwindcss';
@import 'uniwind';
@theme {
--color-primary: #00a8ff;
--color-secondary: #273c75;
--font-normal: 'Roboto-Regular';
--font-bold: 'Roboto-Bold';
}
Font families must specify a single font — React Native doesn't support font fallbacks.
This is the most commonly missed step. Search the entire codebase:
rg "cssInterop|remapProps" -g "*.{ts,tsx,js,jsx}"
Replace every cssInterop() / remapProps() call with Uniwind's withUniwind():
Before (NativeWind):
import { cssInterop } from 'react-native-css-interop';
import { Image } from 'expo-image';
cssInterop(Image, { className: 'style' });
After (Uniwind):
import { withUniwind } from 'uniwind';
import { Image as ExpoImage } from 'expo-image';
export const Image = withUniwind(ExpoImage);
withUniwind automatically maps className → style and other common props. For custom prop mappings:
const StyledProgressBar = withUniwind(ProgressBar, {
width: {
fromClassName: 'widthClassName',
styleProperty: 'width',
},
});
Define wrapped components at module level (not inside render functions). Each component should only be wrapped once:
Used in one file only — define the wrapped component in that same file:
// screens/ProfileScreen.tsx
import { withUniwind } from 'uniwind';
import { BlurView as RNBlurView } from '@react-native-community/blur';
const BlurView = withUniwind(RNBlurView);
export function ProfileScreen() {
return <BlurView className="flex-1" />;
}
Used across multiple files — wrap once in a shared module and re-export:
// components/styled.ts
import { withUniwind } from 'uniwind';
import { Image as ExpoImage } from 'expo-image';
import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
export const Image = withUniwind(ExpoImage);
export const LinearGradient = withUniwind(RNLinearGradient);
Then import from the shared module everywhere:
import { Image, LinearGradient } from '@/components/styled';
Never call withUniwind on the same component in multiple files — wrap once, import everywhere.
IMPORTANT : Do NOT wrap components from react-native or react-native-reanimated with withUniwind — they already support className out of the box. This includes View, Text, Image, ScrollView, FlatList, Pressable, TextInput, , etc. Only use for components (e.g. , , ).
IMPORTANT — accent- prefix for non-style color props : React Native components have props like color, tintColor, backgroundColor that are NOT part of the style object. To set these via Tailwind classes, use the accent- prefix with the corresponding *ClassName prop:
// color prop → colorClassName with accent- prefix
<ActivityIndicator
className="m-4"
size="large"
colorClassName="accent-blue-500 dark:accent-blue-400"
/>
// color prop on Button
<Button
colorClassName="accent-background"
title="Press me"
/>
// tintColor prop → tintColorClassName with accent- prefix
<Image
className="w-6 h-6"
tintColorClassName="accent-red-500"
source={icon}
/>
Rule: className accepts any Tailwind utility for style-based props. For non-style props (color, tintColor, etc.), use {propName}ClassName with the accent- prefix. This applies to all built-in React Native components.
Before (NativeWind JS themes withvars()):
import { vars } from 'nativewind';
export const themes = {
light: vars({
'--color-primary': '#00a8ff',
'--color-typography': '#000',
}),
dark: vars({
'--color-primary': '#273c75',
'--color-typography': '#fff',
}),
};
// In JSX:
<View style={themes[colorScheme]}>
After (Uniwind CSS themes):
@layer theme {
:root {
@variant light {
--color-primary: #00a8ff;
--color-typography: #000;
}
@variant dark {
--color-primary: #273c75;
--color-typography: #fff;
}
}
}
IMPORTANT : All theme variants must define the exact same set of CSS variables. If light defines --color-primary and --color-typography, then dark (and any custom theme) must also define both. Mismatched variables will cause a Uniwind runtime error.
No ThemeProvider wrapper needed. Remove the NativeWind <ThemeProvider> or vars() wrapper from JSX. Keep React Navigation's <ThemeProvider> if used.
If the project used nested theme wrappers to preview or force a theme for a specific subtree (for example a demo card, settings preview, or side-by-side theme comparison), use Uniwind Pro's ScopedTheme instead of changing the global theme:
import { ScopedTheme } from 'uniwind';
<ScopedTheme theme="dark">
<PreviewCard />
</ScopedTheme>
If the project has custom themes beyond light/dark (e.g. ocean, premium), you must:
@variant:@layer theme {
:root {
@variant ocean {
--color-primary: #0ea5e9;
--color-background: #0c4a6e;
}
}
}
2. Register them in metro.config.js via extraThemes (skip light/dark — they are auto-added):
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
polyfills: { rem: 14 },
extraThemes: ['ocean', 'premium'],
});
NativeWind's safe area classes need explicit setup in Uniwind:
import { SafeAreaProvider, SafeAreaListener } from 'react-native-safe-area-context';
import { Uniwind } from 'uniwind';
export default function App() {
return (
<SafeAreaProvider>
<SafeAreaListener
onChange={({ insets }) => {
Uniwind.updateInsets(insets);
}}
>
<View className="pt-safe px-safe">
{/* content */}
</View>
</SafeAreaListener>
</SafeAreaProvider>
);
}
NativeWind uses 14px as the base rem, Uniwind defaults to 16px. Step 4 already sets polyfills: { rem: 14 } in metro config to preserve NativeWind's spacing. If the user explicitly wants Uniwind's default (16px), they can remove the polyfill — but warn them that all spacing/sizing will shift.
Uniwind does NOT auto-deduplicate conflicting classNames (NativeWind did). If your codebase relies on override patterns like className={p-4 ${overrideClass}}, set up a cn utility.
First, check if the project already has a cn helper (common in shadcn/ui projects):
rg "export function cn|export const cn" -g "*.{ts,tsx,js}"
If it exists, keep it as-is. If not, install dependencies and create it:
npm install tailwind-merge clsx
Create lib/cn.ts (or wherever utils live in the project):
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Usage:
import { cn } from '@/lib/cn';
<View className={cn('p-4 bg-white', props.className)} />
<Text className={cn('text-base', isActive && 'text-blue-500', disabled && 'opacity-50')} />
Use cn instead of raw twMerge — it handles conditional classes, arrays, and falsy values via clsx before deduplicating with tailwind-merge.
If the project used NativeWind animated-* / transition class patterns, migrate those to explicit react-native-reanimated usage. Uniwind OSS does not provide NativeWind-style animated class behavior.
Use this migration guide section as the source of truth:
Final sweep — search for and remove any remaining references:
rg "nativewind|NativeWind|native-wind" -g "*.{ts,tsx,js,jsx,json,css}"
Check for:
nativewind in package.json (devDependencies too)react-native-css-interop in package.jsonbabel.config.jsmetro.config.jsnativewind-env.d.ts or nativewind.d.ts filescssInterop() or remapProps() callsDocs: https://docs.uniwind.dev/api/use-uniwind
import { useUniwind } from 'uniwind';
const { theme, hasAdaptiveThemes } = useUniwind();
// theme: current theme name — "light", "dark", "system", or custom
// hasAdaptiveThemes: true if app follows system color scheme
Use for: displaying theme name in UI, conditional rendering by theme, side effects on theme change.
Access theme info without causing re-renders:
import { Uniwind } from 'uniwind';
Uniwind.currentTheme // "light", "dark", "system", or custom
Uniwind.hasAdaptiveThemes // true if following system color scheme
Use for: logging, analytics, imperative logic outside render.
Docs: https://docs.uniwind.dev/api/use-resolve-class-names
Converts Tailwind classes into React Native style objects. Use when working with components that don't support className and can't be wrapped with withUniwind (e.g. react-navigation theme config):
import { useResolveClassNames } from 'uniwind';
const headerStyle = useResolveClassNames('bg-blue-500');
const cardStyle = useResolveClassNames('bg-white dark:bg-gray-900');
<Stack.Navigator
screenOptions={{
headerStyle: headerStyle,
cardStyle: cardStyle,
}}
/>
Docs: https://docs.uniwind.dev/api/use-css-variable
Retrieve CSS variable values programmatically. Variable must be prefixed with -- and match a variable defined in global.css:
import { useCSSVariable } from 'uniwind';
const primaryColor = useCSSVariable('--color-primary');
const spacing = useCSSVariable('--spacing-4');
Use for: animations, third-party library configs, calculations with design tokens.
Docs: https://docs.uniwind.dev/api/css-functions
Define custom utilities using device-aware CSS functions like hairlineWidth(), fontScale(), pixelRatio(). These can be used everywhere (custom CSS classes, @utility, etc.) — but NOT inside @theme {} (which only accepts static values). Use @utility to create reusable Tailwind-style classes:
@utility w-hairline { width: hairlineWidth(); }
@utility h-hairline { height: hairlineWidth(); }
@utility border-hairline { border-width: hairlineWidth(); }
@utility text-scaled { font-size: fontScale(); }
Then use as: <View className="w-hairline h-hairline" />
Docs: https://docs.uniwind.dev/api/platform-select
Apply styles conditionally per platform using ios:, android:, web:, native: prefixes:
<View className="ios:bg-red-500 android:bg-blue-500 web:bg-green-500">
<Text className="ios:text-white android:text-white web:text-black">
Platform-specific styles
</Text>
</View>
Docs: https://docs.uniwind.dev/theming/basics
By default Uniwind follows the system color scheme (adaptive themes). To switch themes programmatically:
import { Uniwind } from 'uniwind';
Uniwind.setTheme('dark'); // force dark
Uniwind.setTheme('light'); // force light
Uniwind.setTheme('system'); // follow system (default)
Uniwind.setTheme('ocean'); // custom theme (must be in extraThemes)
Docs: https://docs.uniwind.dev/api/scoped-themes
Use ScopedTheme when the project needs a different theme for only part of the UI (component previews, themed sections, nested demos) without changing the app-wide theme:
import { ScopedTheme } from 'uniwind';
<View className="gap-3">
<PreviewCard />
<ScopedTheme theme="light">
<PreviewCard />
</ScopedTheme>
<ScopedTheme theme="dark">
<PreviewCard />
</ScopedTheme>
</View>
Important behavior:
ScopedTheme wins (nested scopes are supported)useUniwind, useResolveClassNames, and useCSSVariable resolve against the nearest scoped themewithUniwind-wrapped third-party components inside the scope also resolve themed values from that scopeScopedTheme (must be defined in extraThemes)Docs: https://docs.uniwind.dev/theming/style-based-on-themes
Prefer using CSS variable-based classes over explicit dark:/light: variants. Instead of:
// Avoid this pattern
<View className="light:bg-white dark:bg-black" />
Define a CSS variable and use it directly:
@layer theme {
:root {
@variant light { --color-background: #ffffff; }
@variant dark { --color-background: #000000; }
}
}
// Preferred — automatically adapts to theme
<View className="bg-background" />
This is cleaner, easier to maintain, and works automatically with custom themes too.
Docs: https://docs.uniwind.dev/theming/update-css-variables
Update theme variables at runtime, e.g. based on user preferences or API responses:
import { Uniwind } from 'uniwind';
// Preconfigure theme based on user input or API response
Uniwind.updateCSSVariables('light', {
'--color-primary': '#ff6600',
'--color-background': '#1a1a2e',
});
This pattern should be used only when the app has real runtime theming needs (for example, user-selected brand colors or API-driven themes).
Docs: https://docs.uniwind.dev/tailwind-basics#advanced-pattern-variants-and-compound-variants
For component variants and compound variants, use the tailwind-variants library:
import { tv } from 'tailwind-variants';
const button = tv({
base: 'px-4 py-2 rounded-lg',
variants: {
color: {
primary: 'bg-primary text-white',
secondary: 'bg-secondary text-white',
},
size: {
sm: 'text-sm',
lg: 'text-lg px-6 py-3',
},
},
});
<Pressable className={button({ color: 'primary', size: 'lg' })} />
Docs: https://docs.uniwind.dev/monorepos
If the project is a monorepo, add @source directives in global.css so Tailwind scans packages outside the CSS entry file's directory (only if that directory has components with Tailwind classes):
@import 'tailwindcss';
@import 'uniwind';
@source "../../packages/ui/src";
@source "../../packages/shared/src";
Docs: https://docs.uniwind.dev/faq
Custom Fonts : Uniwind maps className to font-family only — font files must be loaded separately (expo-font plugin in app.json or react-native-asset for bare RN). Font family names in @theme must exactly match filenames (without extension). Use @variant for per-platform fonts (must be inside @layer theme { :root { } }):
@layer theme {
:root {
@variant ios { --font-sans: 'SF Pro Text'; }
@variant android { --font-sans: 'Roboto-Regular'; }
@variant web { --font-sans: 'system-ui'; }
}
}
Data Selectors : Use data-[prop=value]:utility for prop-based styling. Only equality checks supported:
<View data-state={isOpen ? 'open' : 'closed'} className="data-[state=open]:bg-muted/50" />
global.css Location in Expo Router : Place at project root and import in root layout (app/_layout.tsx). If placed in app/, components outside need @source directives. Tailwind scans from global.css location.
Full App Reloads on CSS Changes : Metro can't hot-reload files with many providers. Move global.css import deeper in the component tree (e.g. navigation root or home screen) to fix.
Gradients : Built-in support, no extra deps needed. Use bg-gradient-to-r from-red-500 via-yellow-500 to-green-500. For expo-linear-gradient, use useCSSVariable to get colors — withUniwind won't work since gradient props are arrays.
Style Specificity : Inline style always overrides className. Use className for static styles, inline only for truly dynamic values. Avoid mixing both for the same property.
Serialization Errors (Failed to serialize javascript object): Clear caches: watchman watch-del-all 2>/dev/null; rm -rf node_modules/.cache && npx expo start --clear. Common causes: complex @theme configs, circular CSS variable references.
Metro unstable_enablePackageExports Conflicts : Some apps (crypto etc.) disable this, breaking Uniwind. Use selective resolver:
config.resolver.unstable_enablePackageExports = false;
config.resolver.resolveRequest = (context, moduleName, platform) => {
if (['uniwind', 'culori'].some((prefix) => moduleName.startsWith(prefix))) {
return context.resolveRequest({ ...context, unstable_enablePackageExports: true }, moduleName, platform);
}
return context.resolveRequest(context, moduleName, platform);
};
Safe Area Classes : p-safe, pt-safe, pb-safe, px-safe, py-safe, m-safe, mt-safe, etc. Also supports -or-{value} (min spacing) and -offset-{value} (extra spacing) variants.
Next.js : Not officially supported. Uniwind is for Metro and Vite. Community plugin: uniwind-plugin-next. For Next.js, use standard Tailwind CSS and share design tokens.
Vite : Supported since v1.2.0. Use uniwind/vite plugin alongside @tailwindcss/vite.
UI Kits : HeroUI Native, react-native-reusables and Gluestack 4.1+ works great with Uniwind
data-[prop=value]:utility syntax for conditional styling, similar to NativeWind.react-native-reanimated directly. Uniwind Pro has built-in Reanimated support.After migration, verify:
npx react-native start --reset-cache (clear Metro cache) or with expo npx expo start -cnativewind or react-native-css-interopIMPORTANT : Do NOT guess Uniwind APIs. If you are unsure about any Uniwind API, hook, component, or configuration option, fetch and verify against the official docs: https://docs.uniwind.dev/llms-full.txt
Weekly Installs
656
Repository
GitHub Stars
1.4K
First Seen
Feb 12, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex646
opencode642
gemini-cli640
github-copilot639
amp631
kimi-cli630
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
103,800 周安装
Animated.ViewwithUniwindexpo-imageexpo-linear-gradient@react-native-community/blurvars() imports from nativewind