关于 uniwind
Uniwind 将 Tailwind CSS v4 样式引入 React Native 项目,供使用 React Native 和 Expo 构建应用的开发者使用。它在构建时编译样式,并让核心组件支持 `className`,无需运行时样式处理。
主要功能
- 01为 React Native 提供 Tailwind CSS v4 工具类
- 02构建时编译样式
- 03核心组件支持 `className`
- 04通过 CSS 变量配置主题
- 05支持 Metro 和 Vite 配置
适用场景
- 01用工具类为 React Native 组件设定样式
- 02在应用组件间复用 Tailwind 样式
- 03为 React Native 界面配置主题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ hairyf/ skills --skill uniwind安装后在 Claude Code 中输入/uniwind确认已加载。
附带文件LICENSE.mdSYNC.md
详细介绍
Uniwind — 完整参考
Uniwind 1.7.0+ / Uniwind Pro 1.2.1+ / Tailwind CSS v4 / React Native 0.81+ / Expo SDK 54+
如果用户的版本较低,建议更新到 1.7.0+(免费版)/ 1.2.1+(Pro 版)以获得最佳体验。
Uniwind 将 Tailwind CSS v4 引入 React Native。所有核心 React Native 组件开箱即支持 className 属性。样式在构建时编译 —— 无运行时开销。
关键规则
- 仅支持 Tailwind v4 —— 使用
@import 'tailwindcss',而不是@tailwind base。不支持 Tailwind v3。 - 切勿动态构造 className —— Tailwind 在构建时扫描。
bg-${color}-500不会生效。请使用完整的字符串字面量、映射对象或三元表达式。 - 切勿使用
cssInterop或remapProps—— 这些是 NativeWind 的 API。Uniwind 不会覆盖全局组件。 - 没有
tailwind.config.js—— 所有配置都通过@theme和@layer theme放在global.css中。 - 无需 ThemeProvider —— 直接使用
Uniwind.setTheme()。 withUniwindConfig必须是最外层的 Metro 配置包装器。- 切勿用
withUniwind包装react-native或react-native-reanimated组件 ——View、Text、Pressable、Image、TextInput、ScrollView、FlatList、Switch、Modal、Animated.View、Animated.Text等已内置完整的className支持。用withUniwind包装它们会破坏行为。仅对第三方组件(例如expo-image、expo-blur、moti)使用withUniwind。 - 字体族:仅支持单一字体 —— React Native 不支持回退字体。使用
--font-sans: 'Roboto-Regular',而不是'Roboto', sans-serif。 - 所有主题变体必须定义相同的 CSS 变量集合 —— 如果
light定义了--color-primary,那么dark和每个自定义主题也必须定义。变量不匹配会导致运行时错误。 - 非样式颜色属性必须使用
accent-前缀 —— 这一点至关重要。诸如color(Button、ActivityIndicator)、tintColor(Image)、thumbColor(Switch)、placeholderTextColor(TextInput)等属性不属于style对象。你必须使用对应的{propName}ClassName属性并搭配accent-前缀的类。示例:<ActivityIndicator colorClassName="accent-blue-500" />,而不是<ActivityIndicator className="text-blue-500" />。常规的 Tailwind 颜色类(如text-blue-500)仅对className(映射到style)生效。对于非样式颜色属性,始终使用accent-。 - rem 默认值是 16px —— NativeWind 使用的是 14px。迁移时请在 metro 配置中设置
polyfills: { rem: 14 }。 cssEntryFile必须是相对路径字符串 —— 使用'./global.css',而不是path.resolve(__dirname, 'global.css')。- 混合自定义 CSS 类和 Tailwind 时用
cn()去重 —— Uniwind 不会自动去重。如果某个自定义 CSS 类(.card { padding: 16px })和某个 Tailwind 工具类(p-6)设置了相同的属性,两者都会生效,结果不可预测。存在重叠时,始终用cn('card', 'p-6')包装。 - 支持 important 工具类 —— Tailwind 的 important 修饰符可在 classNames 中使用,
!放在末尾:bg-red-500!、active:bg-red-500!、ios:pt-12!。前置的!bg-red-500语法已弃用。Important 工具类会覆盖针对同一样式属性的非 important 工具类,但内联style仍会覆盖 className。
设置
安装
# or other package manager
bun install uniwind tailwindcss
需要 Tailwind CSS v4+。
global.css
创建一个 CSS 入口文件:
@import 'tailwindcss';
@import 'uniwind';
在你的 App 组件中导入(例如 App.tsx 或 app/_layout.tsx),切勿在 index.ts/index.js 中导入 —— 在那里导入会破坏热重载:
// app/_layout.tsx or App.tsx
import './global.css';
包含 global.css 的目录即应用根目录 —— Tailwind 从此目录开始扫描 className。
Metro 配置
const { getDefaultConfig } = require('expo/metro-config');
// Bare RN: const { getDefaultConfig } = require('@react-native/metro-config');
const { withUniwindConfig } = require('uniwind/metro');
const config = getDefaultConfig(__dirname);
// withUniwindConfig MUST be the OUTERMOST wrapper
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css', // Required — relative path from project root
polyfills: { rem: 16 }, // Optional — base rem value (default 16)
extraThemes: ['ocean', 'sunset'], // Optional — custom themes beyond light/dark
dtsFile: './uniwind-types.d.ts', // Optional — TypeScript types output path
debug: true, // Optional — log unsupported CSS in dev
isTV: false, // Optional — enable TV platform support
});
对于大多数流程,保留默认值,仅提供 cssEntryFile。
包装器顺序 —— Uniwind 必须包装其他所有内容:
// CORRECT
module.exports = withUniwindConfig(withOtherConfig(config, opts), { cssEntryFile: './global.css' });
// WRONG — Uniwind is NOT outermost
module.exports = withOtherConfig(withUniwindConfig(config, { cssEntryFile: './global.css' }), opts);
Vite 配置(v1.2.0+)
如果用户配置了 storybook,请添加额外的 vite 配置:
import tailwindcss from '@tailwindcss/vite';
import { uniwind } from 'uniwind/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
tailwindcss(),
uniwind({
cssEntryFile: './src/global.css',
dtsFile: './src/uniwind-types.d.ts',
}),
],
});
TypeScript
Uniwind 在运行 Metro 后会自动生成一个 .d.ts 文件(默认:./uniwind-types.d.ts)。将其放在 src/ 或 app/ 中以实现自动包含,或添加到 tsconfig.json:
{ "include": ["./uniwind-types.d.ts"] }
如果用户遇到与 className 相关的 TypeScript 错误,只需运行 metro 服务以构建 d.ts 文件。
Expo Router 放置位置
project/
├── app/_layout.tsx ← import '../global.css' here
├── components/
├── global.css ← project root (best location)
└── metro.config.js ← cssEntryFile: './global.css'
如果 global.css 位于 app/ 目录中,为同级目录添加 @source:
@import 'tailwindcss';
@import 'uniwind';
@source '../components';
Tailwind IntelliSense(VS Code / Cursor / Windsurf)
{
"tailwindCSS.classAttributes": [
"class", "className", "headerClassName",
"contentContainerClassName", "columnWrapperClassName",
"endFillColorClassName", "imageClassName", "tintColorClassName",
"ios_backgroundColorClassName", "thumbColorClassName",
"trackColorOnClassName", "trackColorOffClassName",
"selectionColorClassName", "cursorColorClassName",
"underlineColorAndroidClassName", "placeholderTextColorClassName",
"selectionHandleColorClassName", "colorsClassName",
"progressBackgroundColorClassName", "titleColorClassName",
"underlayColorClassName", "colorClassName",
"backdropColorClassName", "backgroundColorClassName",
"statusBarBackgroundColorClassName", "drawerBackgroundColorClassName",
"ListFooterComponentClassName", "ListHeaderComponentClassName"
],
"tailwindCSS.classFunctions": ["useResolveClassNames"]
}
Monorepo 支持
在 global.css 中为 CSS 入口文件目录之外的包添加 @source 指令:
@import 'tailwindcss';
@import 'uniwind';
@source "../../packages/ui/src";
@source "../../packages/shared/src";
对于包含 Uniwind 类的 node_modules 包(例如共享 UI 库)同样需要。
组件绑定
所有核心 React Native 组件开箱即支持 className。部分组件为子样式提供了额外的 className 属性(如 contentContainerClassName)以及非样式颜色属性(需要 accent- 前缀)。
完整参考
图例:标记为 ⚡ 的属性需要 accent- 前缀。括号内的属性为平台特有。
View
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
Text
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
selectionColorClassName | selectionColor | ⚡ accent- |
Pressable
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
支持 active:、disabled:、focus: 状态选择器。
Image
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
tintColorClassName | tintColor | ⚡ accent- |
TextInput
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
cursorColorClassName | cursorColor | ⚡ accent- |
selectionColorClassName | selectionColor | ⚡ accent- |
placeholderTextColorClassName | placeholderTextColor | ⚡ accent- |
selectionHandleColorClassName | selectionHandleColor | ⚡ accent- |
underlineColorAndroidClassName | underlineColorAndroid(Android) | ⚡ accent- |
支持 focus:、active:、disabled: 状态选择器。
ScrollView
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
FlatList
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
columnWrapperClassName | columnWrapperStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
SectionList
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
VirtualizedList
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
Switch
| 属性 | 映射到 | 前缀 |
|---|---|---|
thumbColorClassName | thumbColor | ⚡ accent- |
trackColorOnClassName | trackColor.true(开启) | ⚡ accent- |
trackColorOffClassName | trackColor.false(关闭) | ⚡ accent- |
ios_backgroundColorClassName | ios_backgroundColor(iOS) | ⚡ accent- |
注意:Switch 不支持 className(类型中为 className?: never)。请仅使用上述颜色相关的 className 属性。支持 disabled: 状态选择器。
ActivityIndicator
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
colorClassName | color | ⚡ accent- |
Button
| 属性 | 映射到 | 前缀 |
|---|---|---|
colorClassName | color | ⚡ accent- |
注意:Button 不支持 className(RN Button 没有 style 属性)。
Modal
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
backdropColorClassName | backdropColor | ⚡ accent- |
RefreshControl
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
colorsClassName | colors(Android) | ⚡ accent- |
tintColorClassName | tintColor(iOS) | ⚡ accent- |
titleColorClassName | titleColor(iOS) | ⚡ accent- |
progressBackgroundColorClassName | progressBackgroundColor(Android) | ⚡ accent- |
ImageBackground
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
imageClassName | imageStyle | — |
tintColorClassName | tintColor | ⚡ accent- |
SafeAreaView
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
KeyboardAvoidingView
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
InputAccessoryView
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
backgroundColorClassName | backgroundColor | ⚡ accent- |
TouchableHighlight
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
underlayColorClassName | underlayColor | ⚡ accent- |
支持 active:、disabled: 状态选择器。
TouchableOpacity
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
支持 active:、disabled: 状态选择器。
TouchableNativeFeedback
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
支持 active:、disabled: 状态选择器。
TouchableWithoutFeedback
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
支持 active:、disabled: 状态选择器。
用法示例
import { View, Text, Pressable, TextInput, ScrollView, FlatList, Switch, Image, ActivityIndicator, Modal, RefreshControl, Button } from 'react-native';
// View — basic layout
<View className="flex-1 bg-background p-4">
<Text className="text-foreground text-lg font-bold">Title</Text>
</View>
// Pressable — with press/focus states
<Pressable className="bg-primary px-6 py-3 rounded-lg active:opacity-80 active:bg-primary/90 focus:ring-2">
<Text className="text-white text-center font-semibold">Press Me</Text>
</Pressable>
// TextInput — with focus state and accent- color props
<TextInput
className="border border-border rounded-lg px-4 py-2 text-base text-foreground focus:border-primary"
placeholderTextColorClassName="accent-muted"
selectionColorClassName="accent-primary"
cursorColorClassName="accent-primary"
selectionHandleColorClassName="accent-primary"
underlineColorAndroidClassName="accent-transparent"
placeholder="Enter text..."
/>
// ScrollView — with content container
<ScrollView className="flex-1" contentContainerClassName="p-4 gap-4">
{/* content */}
</ScrollView>
// FlatList — with all sub-style props
<FlatList
className="flex-1"
contentContainerClassName="p-4 gap-3"
columnWrapperClassName="gap-3"
ListHeaderComponentClassName="pb-4"
ListFooterComponentClassName="pt-4"
endFillColorClassName="accent-gray-100"
numColumns={2}
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
/>
// Switch — no className support, use color-specific props only
<Switch
thumbColorClassName="accent-white"
trackColorOnClassName="accent-primary"
trackColorOffClassName="accent-gray-300 dark:accent-gray-700"
ios_backgroundColorClassName="accent-gray-200"
/>
// Image — tint color
<Image className="w-6 h-6" tintColorClassName="accent-primary" source={icon} />
// ActivityIndicator
<ActivityIndicator className="m-4" colorClassName="accent-primary" size="large" />
// Button — only colorClassName (no className)
<Button colorClassName="accent-primary" title="Submit" onPress={handleSubmit} />
// Modal — backdrop color
<Modal className="flex-1" backdropColorClassName="accent-black/50">
{/* content */}
</Modal>
// RefreshControl — platform-specific color props
<RefreshControl
className="p-4"
tintColorClassName="accent-primary"
titleColorClassName="accent-gray-500"
colorsClassName="accent-primary"
progressBackgroundColorClassName="accent-white dark:accent-gray-800"
/>
// ImageBackground — separate image styling
<ImageBackground
className="flex-1 justify-center items-center"
imageClassName="opacity-50"
tintColorClassName="accent-blue-500"
source={bgImage}
>
<Text className="text-white text-2xl font-bold">Overlay</Text>
</ImageBackground>
// KeyboardAvoidingView
<KeyboardAvoidingView
behavior="padding"
className="flex-1 bg-white"
contentContainerClassName="p-4 justify-end"
>
<TextInput className="border border-gray-300 rounded-lg p-3" placeholder="Type..." />
</KeyboardAvoidingView>
// InputAccessoryView
<InputAccessoryView
className="p-4 border-t border-gray-300"
backgroundColorClassName="accent-white dark:accent-gray-800"
>
<Button title="Done" onPress={dismissKeyboard} />
</InputAccessoryView>
// TouchableHighlight — underlay color
<TouchableHighlight
className="bg-blue-500 px-6 py-3 rounded-lg"
underlayColorClassName="accent-blue-600 dark:accent-blue-700"
onPress={handlePress}
>
<Text className="text-white font-semibold">Press Me</Text>
</TouchableHighlight>
accent- 前缀模式
React Native 组件有一些不属于 style 对象的属性,例如 color、tintColor、thumbColor。要通过 Tailwind 类设置这些属性,请使用 accent- 前缀搭配相应的 {propName}ClassName 属性:
// color prop → colorClassName with accent- prefix
<ActivityIndicator colorClassName="accent-blue-500 dark:accent-blue-400" />
<Button colorClassName="accent-primary" title="Submit" />
// tintColor prop → tintColorClassName
<Image className="w-6 h-6" tintColorClassName="accent-red-500" source={icon} />
// thumbColor → thumbColorClassName
<Switch thumbColorClassName="accent-white" trackColorOnClassName="accent-primary" />
// placeholderTextColor → placeholderTextColorClassName
<TextInput placeholderTextColorClassName="accent-gray-400 dark:accent-gray-600" />
关键规则:className 映射到 style 属性——它负责布局、排版、背景、边框等。但 React Native 有许多位于 style 之外的颜色属性(如 color、tintColor、thumbColor、placeholderTextColor)。这些属性需要单独的 {propName}ClassName 属性并配合 accent- 前缀。如果没有 accent-,该类会解析为样式对象——但这些属性期望的是纯颜色字符串。
// WRONG — className sets style, but ActivityIndicator's color is NOT a style prop
<ActivityIndicator className="text-blue-500" /> // color will NOT be set
// CORRECT — use the dedicated colorClassName prop with accent- prefix
<ActivityIndicator colorClassName="accent-blue-500" /> // color IS set to #3b82f6
// WRONG — tintColor is not a style prop on Image
<Image className="tint-blue-500" source={icon} /> // won't work
// CORRECT
<Image tintColorClassName="accent-blue-500" source={icon} />
为第三方组件设置样式
withUniwind(推荐)
在模块级别包装一次,然后在各处使用 className:
import { withUniwind } from 'uniwind';
import { Image as ExpoImage } from 'expo-image';
import { BlurView as RNBlurView } from 'expo-blur';
import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
// Module-level wrapping (NEVER inside render functions)
export const Image = withUniwind(ExpoImage);
export const BlurView = withUniwind(RNBlurView);
export const LinearGradient = withUniwind(RNLinearGradient);
withUniwind 会自动映射:
style→className{name}Style→{name}ClassName{name}Color→{name}ColorClassName(带 accent- 前缀)
自定义 prop 映射:
const StyledProgressBar = withUniwind(ProgressBar, {
width: {
fromClassName: 'widthClassName',
styleProperty: 'width',
},
});
使用模式:
- 仅在一个文件中使用 — 在同一文件中定义包装后的组件
- 在多个文件中使用 — 在共享模块(例如
components/styled.ts)中包装一次并重新导出
// components/styled.ts
import { withUniwind } from 'uniwind';
import { Image as ExpoImage } from 'expo-image';
export const Image = withUniwind(ExpoImage);
// Then import everywhere:
import { Image } from '@/components/styled';
绝不在多个文件中对同一组件调用 withUniwind。
关键:不要对来自 react-native 或 react-native-reanimated 的组件使用 withUniwind。这些组件已经内置了 className 支持:
// WRONG — View already supports className natively
const StyledView = withUniwind(View); // DO NOT DO THIS
const StyledText = withUniwind(Text); // DO NOT DO THIS
const StyledAnimatedView = withUniwind(Animated.View); // DO NOT DO THIS
// CORRECT — only wrap third-party components
const StyledExpoImage = withUniwind(ExpoImage); // expo-image
const StyledBlurView = withUniwind(BlurView); // expo-blur
const StyledMotiView = withUniwind(MotiView); // moti
useResolveClassNames
将 Tailwind 类字符串转换为 React Native 样式对象。用于一次性场景或仅接受 style 的组件:
import { useResolveClassNames } from 'uniwind';
const headerStyle = useResolveClassNames('bg-primary p-4');
const cardStyle = useResolveClassNames('bg-card dark:bg-card rounded-lg shadow-sm');
// React Navigation screen options
<Stack.Navigator screenOptions={{ headerStyle, cardStyle }} />
对比
| 特性 | withUniwind | useResolveClassNames |
|---|---|---|
| 设置 | 每个组件一次 | 每次使用时 |
| 性能 | 已优化 | 稍慢 |
| 最适合 | 可复用组件 | 一次性场景、导航配置 |
| 语法 | className="..." | style={...} |
动态 ClassName
绝不要这样做(Tailwind 在构建时扫描)
// BROKEN — template literal with variable
<View className={`bg-${color}-500`} />
<Text className={`text-${size}`} />
正确的模式
// Ternary with complete class names
<View className={isActive ? 'bg-primary' : 'bg-muted'} />
// Mapping object
const colorMap = {
primary: 'bg-blue-500 text-white',
danger: 'bg-red-500 text-white',
ghost: 'bg-transparent text-foreground',
};
<Pressable className={colorMap[variant]} />
// Array join for multiple conditions
<View className={[
'p-4 rounded-lg',
isActive && 'bg-primary',
isDisabled && 'opacity-50',
].filter(Boolean).join(' ')} />
tailwind-variants(tv)
用于带有变体和复合变体的复杂组件样式:
import { tv } from 'tailwind-variants';
const button = tv({
base: 'font-semibold rounded-lg px-4 py-2 items-center justify-center',
variants: {
color: {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-500 text-white',
danger: 'bg-red-500 text-white',
ghost: 'bg-transparent text-foreground border border-border',
},
size: {
sm: 'text-sm px-3 py-1.5',
md: 'text-base px-4 py-2',
lg: 'text-lg px-6 py-3',
},
disabled: {
true: 'opacity-50',
},
},
compoundVariants: [
{ color: 'primary', size: 'lg', class: 'bg-blue-600' },
],
defaultVariants: { color: 'primary', size: 'md' },
});
<Pressable className={button({ color: 'primary', size: 'lg' })}>
<Text className="text-white font-semibold">Click</Text>
</Pressable>
cn 工具 — 类去重
Uniwind 不会自动去重冲突的 className。这意味着如果同一属性出现在多个类中,两者都会被应用,结果不可预测。当将自定义 CSS 类与 Tailwind 工具类混用时,这一点尤其关键。
设置
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));
}
何时需要 cn
- 合并 className props — 组件接受可能冲突的外部 className:
import { cn } from '@/lib/cn';
<View className={cn('p-4 bg-white', props.className)} />
<Text className={cn('text-base', isActive && 'text-primary', disabled && 'opacity-50')} />
- 关键:将自定义 CSS 类与 Tailwind 工具类混用 — 如果你的自定义 CSS 类设置了某个属性,而某个 Tailwind 工具类也设置了该属性,则必须使用
cn()去重:
/* global.css */
.card {
background-color: white;
border-radius: 12px;
padding: 16px;
}
// WRONG — both .card (padding: 16px) and p-6 (padding: 24px) apply, result is unpredictable
<View className="card p-6" />
// CORRECT — cn deduplicates, p-6 wins over .card's padding
<View className={cn('card', 'p-6')} />
- tv() 输出与额外类组合 — tv 已经处理了自己的变体,但如果你在此基础上添加更多类:
<Pressable className={cn(button({ color: 'primary' }), props.className)} />
何时不需要 cn
- 无冲突的静态 className:
<View className="flex-1 p-4 bg-white" /> - 无重叠 Tailwind 的单个自定义 CSS 类:
<View className="card-shadow mt-4" />(如果 card-shadow 只设置 box-shadow,且没有 Tailwind 类也设置它)
重要工具类与样式优先级
Uniwind 支持 Tailwind 的重要修饰符(!),用于必须覆盖同一样式属性上另一个工具类的工具类。
import { View, Pressable } from 'react-native';
// bg-red-500! has higher priority than bg-blue-500
<View className="bg-blue-500 bg-red-500!" />;
// Important utilities work with state and platform variants
<Pressable className="bg-blue-500 active:bg-red-500!" />;
<View className="pt-4 ios:pt-12! android:pt-8!" />;
优先级规则:
- 重要工具类(
bg-red-500!)在同一属性上覆盖非重要工具类(bg-blue-500)。 - 重要变体正常工作:
active:bg-red-500!、ios:pt-12!、dark:text-white!。 - 内联
style始终优先,即使面对重要的 className 工具类:<View className="bg-red-500!" style={{ backgroundColor: 'blue' }} />渲染为蓝色。 - 谨慎使用
!。对于可复用组件和使用方覆盖,优先使用cn()搭配tailwind-merge。
主题
快速设置(dark: 前缀)
立即可用 — 无需配置:
<View className="bg-white dark:bg-gray-900">
<Text className="text-black dark:text-white">Themed</Text>
</View>
最适合小型应用和原型设计。无法扩展到自定义主题。
可扩展设置(CSS 变量)
在 global.css 中定义,在各地使用,无需 dark: 前缀:
@layer theme {
:root {
@variant light {
--color-background: #ffffff;
--color-foreground: #111827;
--color-foreground-secondary: #6b7280;
--color-card: #ffffff;
--color-border: #e5e7eb;
--color-muted: #9ca3af;
--color-primary: #3b82f6;
--color-danger: #ef4444;
--color-success: #10b981;
}
@variant dark {
--color-background: #000000;
--color-foreground: #ffffff;
--color-foreground-secondary: #9ca3af;
--color-card: #1f2937;
--color-border: #374151;
--color-muted: #6b7280;
--color-primary: #3b82f6;
--color-danger: #ef4444;
--color-success: #10b981;
}
}
}
// Auto-adapts to current theme — no dark: prefix needed
<View className="bg-card border border-border p-4 rounded-lg">
<Text className="text-foreground text-lg font-bold">Title</Text>
<Text className="text-muted mt-2">Subtitle</Text>
</View>
变量命名:--color-background → bg-background、text-background。
优先使用 CSS 变量而非显式的 dark: 变体 — 它们更简洁、更易维护,并自动兼容自定义主题。
自定义主题
第 1 步 — 在 global.css 中定义:
@layer theme {
:root {
@variant light { /* ... */ }
@variant dark { /* ... */ }
@variant ocean {
--color-background: #0c4a6e;
--color-foreground: #e0f2fe;
--color-primary: #06b6d4;
--color-card: #0e7490;
--color-border: #155e75;
/* Must define ALL the same variables as light/dark */
}
}
}
第 2 步 — 在 metro.config.js 中注册(排除 light/dark — 它们是自动的):
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
extraThemes: ['ocean'],
});
添加主题后重启 Metro。
第 3 步 — 使用:
Uniwind.setTheme('ocean');
主题 API
import { Uniwind, useUniwind } from 'uniwind';
// Imperative (no re-render)
Uniwind.setTheme('dark'); // Force dark
Uniwind.setTheme('light'); // Force light
Uniwind.setTheme('system'); // Follow device (re-enables adaptive themes)
Uniwind.setTheme('ocean'); // Custom theme (must be in extraThemes)
Uniwind.currentTheme; // Current theme name
Uniwind.hasAdaptiveThemes; // true if following system
// Reactive hook (re-renders on change)
const { theme, hasAdaptiveThemes } = useUniwind();
Uniwind.setTheme('light') / setTheme('dark') 也会调用 Appearance.setColorScheme 以同步原生组件(Alert、Modal、系统对话框)。
默认情况下,Uniwind 使用 "system" 主题——跟随设备配色方案。如果用户想要覆盖它,只需 调用 Uniwind.setTheme 并传入所需的主题。这可以在 React 组件之上完成,以避免在运行时切换主题。
主题切换器示例
import { View, Pressable, Text, ScrollView } from 'react-native';
import { Uniwind, useUniwind } from 'uniwind';
export const ThemeSwitcher = () => {
const { theme, hasAdaptiveThemes } = useUniwind();
const activeTheme = hasAdaptiveThemes ? 'system' : theme;
const themes = [
{ name: 'light', label: 'Light' },
{ name: 'dark', label: 'Dark' },
{ name: 'system', label: 'System' },
];
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className="flex-row gap-2 p-4">
{themes.map((t) => (
<Pressable
key={t.name}
onPress={() => Uniwind.setTheme(t.name)}
className={`px-4 py-3 rounded-lg items-center ${
activeTheme === t.name ? 'bg-primary' : 'bg-card border border-border'
}`}
>
<Text className={`text-sm ${
activeTheme === t.name ? 'text-white' : 'text-foreground'
}`}>
{t.label}
</Text>
</Pressable>
))}
</View>
</ScrollView>
);
};
ScopedTheme
将不同的主题应用于子树,而不更改全局主题:
import { ScopedTheme } from 'uniwind';
<View className="gap-3">
<PreviewCard />
<ScopedTheme theme="dark">
<PreviewCard /> {/* Renders with dark theme */}
</ScopedTheme>
<ScopedTheme theme="ocean">
<PreviewCard /> {/* Renders with ocean theme */}
</ScopedTheme>
</View>
- 最近的
ScopedTheme生效(支持嵌套作用域) - Hooks(
useUniwind、useResolveClassNames、useCSSVariable)会针对最近的作用域主题进行解析 - 作用域内被
withUniwind包裹的组件也会解析作用域主题的值 - 自定义主题需要在
extraThemes中注册
useCSSVariable
在 JavaScript 中访问 CSS 变量值:
import { useCSSVariable } from 'uniwind';
const primaryColor = useCSSVariable('--color-primary');
const spacing = useCSSVariable('--spacing-4');
// Multiple variables at once
const [bg, fg] = useCSSVariable(['--color-background', '--color-foreground']) as [string, string]
用于:动画、图表库、第三方组件配置、使用设计令牌进行计算。
需要将 useCSSVariable 的结果进行类型转换,因为它可能返回:string | number | undefined。
Uniwind 不知道给定变量是否存在以及它是什么类型,因此它返回联合类型。
getCSSVariable
在 React 之外读取 CSS 变量值(事件处理器、异步回调、工具模块、worklets)。在 Uniwind 1.6.4+ 中可用。
import { Uniwind } from 'uniwind';
const primary = Uniwind.getCSSVariable('--color-primary');
const [bg, fg] = Uniwind.getCSSVariable(['--color-background', '--color-foreground']) as [string, string];
值规则与 useCSSVariable 相同(变量必须在 className 中使用,或在 @theme static 中声明)。返回类型也相同:string | number | undefined。按需进行类型转换。
非响应式——值只读取一次。对于组件内的响应式值,请使用 useCSSVariable。将 getCSSVariable 用于一次性读取(onPress 处理器、工具函数、原生模块配置)。
运行时 CSS 变量更新
在运行时更新主题变量(例如,用户选择的品牌颜色或由 API 驱动的主题):
Uniwind.updateCSSVariables('light', {
'--color-primary': '#ff6600',
'--color-background': '#fafafa',
});
更新是针对特定主题的,并立即生效。
@theme static
用于不在 classNames 中使用的仅限 JS 的值:
@theme static {
--chart-line-width: 2;
--chart-dot-radius: 4;
--animation-duration: 300;
}
通过 useCSSVariable('--chart-line-width') 访问。用于:图表配置、动画时长、原生模块值。
OKLCH 颜色支持
感知均匀的颜色格式——更宽的色域、一致的光亮度:
@layer theme {
:root {
@variant light {
--color-primary: oklch(0.5 0.2 240);
--color-background: oklch(1 0 0);
}
@variant dark {
--color-primary: oklch(0.6 0.2 240);
--color-background: oklch(0.13 0.004 17.69);
}
}
}
Display P3 颜色支持
适用于支持 P3 色彩空间的设备(大多数现代 iPhone 和 Mac)的广色域颜色格式。Uniwind 解析 color(display-p3 ...) 值并将其转换为原生使用:
@layer theme {
:root {
@variant light {
--color-primary: color(display-p3 0.2 0.4 1);
--color-accent: color(display-p3 1 0.3 0.3);
}
@variant dark {
--color-primary: color(display-p3 0.3 0.5 1);
--color-accent: color(display-p3 1 0.4 0.4);
}
}
}
平台选择器
直接在 className 中应用平台特定的样式:
// Individual platforms
<View className="ios:bg-red-500 android:bg-blue-500 web:bg-green-500" />
// native: shorthand (iOS + Android)
<View className="native:bg-blue-500 web:bg-gray-500" />
// TV platforms
<View className="tv:p-8 android-tv:bg-black apple-tv:bg-gray-900" />
// Combine with other utilities
<View className="p-4 ios:pt-12 android:pt-6 web:pt-4" />
在 @layer theme 中使用平台变体来设置全局值(使用 @variant,而不是 @media):
@layer theme {
:root {
@variant ios { --font-sans: 'SF Pro Text'; }
@variant android { --font-sans: 'Roboto-Regular'; }
@variant web { --font-sans: 'Inter'; }
}
}
优先使用平台选择器而不是 Platform.select() —— 语法更简洁,无需导入。
数据选择器
使用 data-[prop=value]:utility 根据 prop 值设置样式:
// Boolean props
<Pressable
data-selected={isSelected}
className="border rounded px-3 py-2 data-[selected=true]:ring-2 data-[selected=true]:ring-primary"
/>
// String props
<View
data-state={isOpen ? 'open' : 'closed'}
className="p-4 data-[state=open]:bg-muted/50 data-[state=closed]:bg-transparent"
/>
// Tabs pattern
<Pressable
data-selected={route.key === current}
className="px-4 py-2 rounded-md text-foreground/60
data-[selected=true]:bg-primary data-[selected=true]:text-white"
>
<Text>{route.title}</Text>
</Pressable>
// Toggle pattern
<Pressable
data-checked={enabled}
className="h-6 w-10 rounded-full bg-muted data-[checked=true]:bg-primary"
>
<View className="h-5 w-5 rounded-full bg-background translate-x-0 data-[checked=true]:translate-x-4" />
</Pressable>
规则:
- 仅支持相等选择器(
data-[prop=value]) - 不支持仅存在性选择器(
data-[prop]—— 不支持) - 不支持
has-data-*父选择器(React Native 中不支持) - 布尔值同时匹配布尔形式和字符串形式
交互状态
// active: — when pressed
<Pressable className="bg-primary active:bg-primary/80 active:opacity-90 active:scale-95">
<Text className="text-white">Press me</Text>
</Pressable>
// disabled: — when disabled prop is true
<Pressable
disabled={isLoading}
className="bg-primary disabled:bg-gray-300 disabled:opacity-50"
>
<Text className="text-white disabled:text-gray-500">Submit</Text>
</Pressable>
// focus: — keyboard/accessibility focus
<TextInput
className="border border-border rounded-lg px-4 py-2 focus:border-primary focus:ring-2 focus:ring-primary/20"
/>
<Pressable className="bg-card rounded-lg p-4 focus:ring-2 focus:ring-primary">
<Text className="text-foreground">Focusable</Text>
</Pressable>
支持状态的组件:
- Pressable:
active:、disabled:、focus: - TextInput:
active:、disabled:、focus: - Switch:
disabled: - Text:
active:、disabled: - TouchableOpacity / TouchableHighlight / TouchableNativeFeedback / TouchableWithoutFeedback:
active:、disabled:
响应式断点
移动优先 —— 无前缀样式适用于所有尺寸,带前缀样式在该断点及以上生效:
| 前缀 | 最小宽度 | 典型设备 |
|---|---|---|
| (无) | 0px | 所有(移动端) |
sm: | 640px | 大屏手机 |
md: | 768px | 平板 |
lg: | 1024px | 横屏平板 |
xl: | 1280px | 桌面端 |
2xl: | 1536px | 大屏桌面端 |
// Responsive padding and typography
<View className="p-4 sm:p-6 lg:p-8">
<Text className="text-base sm:text-lg lg:text-xl font-bold">Responsive</Text>
</View>
// Responsive grid (1 col → 2 col → 3 col)
<View className="flex-row flex-wrap">
<View className="w-full sm:w-1/2 lg:w-1/3 p-2">
<View className="bg-card p-4 rounded"><Text>Item</Text></View>
</View>
</View>
// Responsive visibility
<View className="hidden sm:flex flex-row gap-4">
<Text>Visible on tablet+</Text>
</View>
<View className="flex sm:hidden">
<Text>Mobile only</Text>
</View>
自定义断点:
@theme {
--breakpoint-xs: 480px;
--breakpoint-tablet: 820px;
--breakpoint-3xl: 1920px;
}
用法:xs:p-2 tablet:p-4 3xl:p-8
采用移动优先设计 —— 从基础样式(无前缀)开始,再用断点增强:
// CORRECT — mobile-first
<View className="w-full sm:w-3/4 md:w-1/2 lg:w-1/3" />
// WRONG — desktop-first (reversed order is confusing and fragile)
<View className="w-full lg:w-1/2 md:w-3/4 sm:w-full" />
安全区域工具类
内边距
| 类名 | 说明 |
|---|---|
p-safe | 所有边 |
pt-safe / pb-safe / pl-safe / pr-safe | 单边 |
px-safe / py-safe | 水平 / 垂直 |
外边距
| 类名 | 说明 |
|---|---|
m-safe | 所有边 |
mt-safe / mb-safe / ml-safe / mr-safe | 单边 |
mx-safe / my-safe | 水平 / 垂直 |
定位
| 类名 | 说明 |
|---|---|
inset-safe | 所有边 |
top-safe / bottom-safe / left-safe / right-safe | 单边 |
x-safe / y-safe | 水平 / 垂直 inset |
复合变体
| 模式 | 行为 | 示例 |
|---|---|---|
{prop}-safe-or-{value} | Math.max(inset, value) —— 确保最小间距 | pt-safe-or-4 |
{prop}-safe-offset-{value} | inset + value —— 在 inset 基础上增加额外间距 | pb-safe-offset-4 |
设置
Uniwind Free(默认) —— 需要 react-native-safe-area-context 来更新 inset。
将你的 App 组件包裹在 SafeAreaProvider 和 SafeAreaListener 中,并在 onChange 回调中调用 Uniwind.updateInsets(insets):
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>
);
}
Uniwind Pro —— 自动完成,无需设置。Inset 从原生层注入。
CSS 函数
Uniwind 提供用于设备感知和主题感知样式的 CSS 函数。它们可以在任何地方使用(自定义 CSS 类、@utility 等)—— 但不能在 @theme {} 内使用(后者只接受静态值)。使用 @utility 创建可复用的 Tailwind 风格工具类:
hairlineWidth()
返回设备可显示的最细线宽。用于细微的边框和分隔线。
@utility h-hairline { height: hairlineWidth(); }
@utility border-hairline { border-width: hairlineWidth(); }
@utility w-hairline { width: calc(hairlineWidth() * 10); }
<View className="h-hairline bg-gray-300" />
<View className="border-hairline border-gray-200 rounded-lg p-4" />
fontScale(multiplier?)
将基础值乘以设备的字体缩放无障碍设置。确保文本遵循用户对更大或更小字体的偏好。
fontScale()—— 使用乘数 1(设备字体缩放 × 1)fontScale(0.9)—— 更小的缩放fontScale(1.2)—— 更大的缩放
@utility text-sm-scaled { font-size: fontScale(0.9); }
@utility text-base-scaled { font-size: fontScale(); }
@utility text-lg-scaled { font-size: fontScale(1.2); }
<Text className="text-sm-scaled text-gray-600">Small accessible text</Text>
<Text className="text-base-scaled">Regular accessible text</Text>
pixelRatio(multiplier?)
将值乘以设备的像素比。创建跨屏幕密度缩放的像素级精准设计。
pixelRatio()—— 使用乘数 1(设备像素比 × 1)pixelRatio(2)—— 像素比翻倍
@utility w-icon { width: pixelRatio(); }
@utility w-avatar { width: pixelRatio(2); }
<Image source={{ uri: 'avatar.png' }} className="w-avatar rounded-full" />
light-dark(lightValue, darkValue)
根据当前主题模式返回不同的值。主题变化时自动适配 —— 无需手动切换逻辑。
- 第一个参数:浅色主题的值
- 第二个参数:深色主题的值
@utility bg-adaptive { background-color: light-dark(#ffffff, #1f2937); }
@utility text-adaptive { color: light-dark(#111827, #f9fafb); }
@utility border-adaptive { border-color: light-dark(#e5e7eb, #374151); }
<View className="bg-adaptive border-adaptive border rounded-lg p-4">
<Text className="text-adaptive">Adapts to light/dark theme</Text>
</View>
也可在自定义 CSS 类中使用(不仅限于 @utility):
.adaptive-card {
background-color: light-dark(#ffffff, #1f2937);
color: light-dark(#111827, #f9fafb);
}
自定义 CSS 与工具类
自定义 CSS 类
Uniwind 支持在 global.css 中定义自定义 CSS 类名。它们在构建时编译 —— 没有运行时开销。当你需要难以用 Tailwind 工具类表达的样式(例如复杂的 box-shadow、多属性组合)时使用它们。
/* global.css */
.card-shadow {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.adaptive-surface {
background-color: light-dark(#ffffff, #1f2937);
color: light-dark(#111827, #f9fafb);
}
.container {
flex: 1;
width: 100%;
max-width: 1200px;
}
像任何 Tailwind 类一样通过 className 应用:
<View className="card-shadow" />
将自定义 CSS 与 Tailwind 混用
你可以在单个 className 中结合自定义 CSS 类与 Tailwind 工具类:
<View className="card-shadow p-4 m-2">
<Text className="adaptive-surface mb-2">{title}</Text>
<View className="container flex-row">{children}</View>
</View>
警告:如果自定义 CSS 类与 Tailwind 工具类设置了相同的属性,你必须使用 cn() 去重。不使用 cn() 时,两个值都会生效,结果不可预测:
// WRONG — .container sets flex:1, and flex-1 also sets flex:1 (harmless but wasteful)
// WRONG — .container sets width:100%, and w-full also sets width:100% (redundant)
// DANGEROUS — .card-shadow sets border-radius:12px, and rounded-2xl sets border-radius:16px — CONFLICT!
<View className="card-shadow rounded-2xl" />
// CORRECT — cn ensures rounded-2xl wins
import { cn } from '@/lib/cn';
<View className={cn('card-shadow', 'rounded-2xl')} />
经验法则:如果你的自定义 CSS 类设置的属性可能与你同样会使用的 Tailwind 工具类重叠,始终用 cn() 包裹。完整设置参见 cn 工具 章节。
自定义 CSS 指南
- 保持选择器扁平 — 不要深层嵌套或使用子选择器
- 对于简单的单属性样式,优先使用 Tailwind 工具类
- 对于在 className 中会显得冗长的复杂或多属性组合,使用自定义类
- 对主题感知的自定义类使用
light-dark() - 自定义类非常适合存放不符合 Tailwind 命名规范的共享设计令牌(例如
.card、.chip、.badge-dot)
自定义工具类(@utility)
@utility 指令创建的实用类与内置 Tailwind 类的工作方式完全一致。三种主要用例:
1. 变量驱动的工具类(运行时注入的值)
创建一个工具类,其值来自通过 updateCSSVariables 在运行时注入的 CSS 变量。使用 @theme static 声明该变量,以便 Uniwind 在其更新之前就能追踪它:
/* global.css */
@theme static {
--header-height: 0px;
}
@utility p-safe-header {
padding-top: var(--header-height);
}
在运行时注入真实值(例如,来自 react-navigation 的布局事件):
import { Uniwind } from 'uniwind'
// e.g., inside a navigation layout listener
Uniwind.updateCSSVariables(Uniwind.currentTheme, {
'--header-height': headerHeight,
})
<View className="p-safe-header flex-1" />
2. 全新的工具类(没有 Tailwind 等价物)
用于没有内置 Tailwind 类的样式:
@utility h-hairline { height: hairlineWidth(); }
@utility text-scaled { font-size: fontScale(); }
@utility card-shadow {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
像任何 Tailwind 类一样使用:<View className="h-hairline card-shadow" />
3. 覆盖已有的 Tailwind 工具类
使用 @utility 完全替换某个内置类的行为。例如:让 border 始终使用 --color-primary:
@utility border {
border-width: 1px;
border-style: solid;
border-color: var(--color-primary);
}
@theme 指令
在 global.css 中自定义 Tailwind 设计令牌:
@theme {
/* Colors */
--color-primary: #3b82f6;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a8a;
/* Typography */
--font-sans: 'Roboto-Regular';
--font-sans-medium: 'Roboto-Medium';
--font-sans-bold: 'Roboto-Bold';
--font-mono: 'FiraCode-Regular';
/* Spacing & sizing */
--text-base: 15px;
--spacing-4: 16px;
--radius-lg: 12px;
/* Breakpoints */
--breakpoint-tablet: 820px;
}
用法:bg-brand-500、text-brand-900、font-sans、font-mono、rounded-lg。
字体
React Native 每个字族需要单一字体 — 不支持回退:
@theme {
--font-sans: 'Roboto-Regular';
--font-sans-bold: 'Roboto-Bold';
--font-mono: 'FiraCode-Regular';
}
字体名称必须完全匹配字体文件名(不含扩展名)。
Expo:在 app.json 中通过 expo-font 插件配置字体,然后在 CSS 中引用。
裸 RN:使用 react-native-asset 链接字体,CSS 配置相同。
平台特定字体(使用 @variant,而非 @media):
@layer theme {
:root {
@variant ios { --font-sans: 'SF Pro Text'; }
@variant android { --font-sans: 'Roboto-Regular'; }
@variant web { --font-sans: 'system-ui'; }
}
}
渐变
内置支持 — 无需额外依赖:
<View className="bg-gradient-to-r from-red-500 via-yellow-500 to-green-500 p-4 rounded-lg">
<Text className="text-white font-bold">Gradient</Text>
</View>
对于 expo-linear-gradient,你可以用 withUniwind 包裹它,以实现基于 className 的布局与样式(padding、border-radius、flex 等),但 colors 属性是一个数组,无法通过 className 解析 — 必须显式提供。使用 useCSSVariable 获取主题感知的颜色:
import { useCSSVariable } from 'uniwind';
import { withUniwind } from 'uniwind';
import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
const LinearGradient = withUniwind(RNLinearGradient);
function GradientCard() {
// useCSSVariable returns string | number | undefined
const primary = useCSSVariable('--color-primary');
const secondary = useCSSVariable('--color-secondary');
// Guard against undefined — LinearGradient.colors requires valid ColorValues
if (!primary || !secondary) {
return null;
}
return (
<LinearGradient
className="flex-1 rounded-2xl p-6"
colors={[primary as string, secondary as string]}
>
<Text className="text-white font-bold">Themed gradient</Text>
</LinearGradient>
);
}
或者,从共享模块导出一个包裹后的组件以便复用:
// components/styled.ts
import { withUniwind } from 'uniwind';
import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
export const LinearGradient = withUniwind(RNLinearGradient);
// usage — className handles layout, colors still passed manually
import { LinearGradient } from '@/components/styled';
<LinearGradient className="rounded-xl p-4" colors={['#ff6b6b', '#4ecdc4']}>
<Text className="text-white">Static gradient</Text>
</LinearGradient>
React Navigation 集成
对于只接受 style 对象的屏幕选项,使用 useResolveClassNames:
import { useResolveClassNames } from 'uniwind';
function Layout() {
const headerStyle = useResolveClassNames('bg-background');
const headerTitleStyle = useResolveClassNames('text-foreground font-bold');
return (
<Stack.Navigator
screenOptions={{
headerStyle,
headerTitleStyle,
}}
/>
);
}
如果已经在使用 React Navigation 的 <ThemeProvider>,请保留它 — 它管理导航特定的主题。
UI 套件兼容性
- HeroUI Native:可与 Uniwind 配合使用。内部使用
tailwind-variants(tv)。直接在 HeroUI 组件上应用className。Bun 用户:Bun 对node_modules使用符号链接,这可能导致 Tailwind 的 Oxide 扫描器在生产构建中遗漏库类。修复方法:在@source中使用解析后的路径并提升该包:@source "../../node_modules/heroui-native/lib";# .npmrc public-hoist-pattern[]=heroui-native - react-native-reusables:兼容。
- Gluestack v4.1+:兼容。
- Lucide React Native:使用
withUniwind(LucideIcon)并配合colorClassName="accent-blue-500"设置图标颜色。适用于所有 Lucide 图标。 - @shopify/flash-list:使用
withUniwind(FlashList)以支持className和contentContainerClassName。注意:withUniwind会丢失ref上的泛型类型参数 — 如有需要请手动转换。
使用语义化颜色令牌(bg-primary、text-foreground)以在各 UI 套件之间保持主题一致性。
支持的类与不支持的类
React Native 使用 Yoga 布局引擎。与 Web CSS 的关键区别:
- 没有 CSS 级联/继承 — 样式不会从父级继承
- 默认使用 Flexbox — 所有视图都使用 flexbox,且
flexDirection: 'column' - CSS 属性有限 — 没有 float、grid、伪元素
内置额外工具类
Uniwind 为标准 Tailwind 未覆盖的 React Native 特性提供了额外的工具类:
| 类 | 效果 |
|---|---|
border-continuous | 设置 borderCurve: 'continuous' — 平滑的超椭圆圆角(iOS) |
border-circular | 设置 borderCurve: 'circular' — 标准的圆形圆角(iOS 默认) |
// Smooth iOS-style rounded corners (like SwiftUI's .continuous)
<View className="rounded-2xl border-continuous bg-card p-4">
<Text className="text-foreground">Smooth corners</Text>
</View>
支持(所有标准 Tailwind)
布局、间距、尺寸、排版、颜色、边框、效果、flexbox、定位、变换、交互状态。
不支持(Web 专属,会被静默忽略)
hover:visited:— 请改用 Pressable 的active:before:after:placeholder:— 伪元素float-*clear-*columns-*print:screen:break-before-*break-after-*break-inside-*
Uniwind Pro
付费升级版,100% API 兼容。基于第二代 C++ 引擎构建,面向对性能有极致要求的应用。阶梯定价(按年计费):$99/席位(1-3),$49(4-6),$29(7-15),$1(16+)。价格与许可:https://uniwind.dev/pricing
价格与许可
- 按席位阶梯定价(按年计费,除非适用否则不含 VAT):1-3 个席位 $99,4-6 个 $49,7-15 个 $29,16+ 个 $1
- 个人许可:每位工程师的个人 Pro 许可
- 团队许可:单一密钥管理 — 可即时添加或移除成员
- CI/CD 许可:全面支持自动化与无头构建环境
- 企业版:可提供定制方案(联系 [email protected])
- 优先支持:关键问题按优先响应时间解决
概述
- C++ 样式引擎:基于第二代 Unistyles C++ 引擎打造。直接将样式注入 ShadowTree,而不触发 React 重新渲染 — 在 classNames 与原生层之间建立一条直接、优化的高速通道
- 性能:基准测试约为 ~55ms(对比 StyleSheet 49ms、传统 Uniwind 81ms、NativeWind 197ms)— 接近原生速度
- 55+ 个 className 属性在 20 个组件中更新时无需重新渲染(所有组件绑定均列于上方)
- Reanimated 动画:通过 className 使用
animate-*和transition-*(Reanimated v4) - 原生 insets 与运行时值:自动注入安全区域、设备旋转与字体大小更新 — 无需设置
SafeAreaListener - 主题过渡:切换主题时使用原生动画过渡(淡入淡出、滑动、圆形遮罩)
- 组变体:Tailwind
group-active:*/group-focus:*通过 C++ shadow tree 传播父级交互状态,零重新渲染 - 默认样式:实验性
1.2.0+功能,可通过 CSS 选择器(如View { ... }和Text { ... })为默认 React Native 组件设置样式 - 优先支持:别让技术障碍拖慢你的团队
包:在 package.json 中使用 "uniwind": "npm:uniwind-pro@latest"。
安装
-
在
package.json中设置依赖别名:{ "dependencies": { "uniwind": "npm:uniwind-pro@latest" } } -
安装 peer 依赖:
npm install react-native-nitro-modules react-native-reanimated react-native-worklets -
进行身份验证:
npx uniwind-pro(交互式 — 选择 "Login with GitHub") -
添加 Babel 插件 — 将
react-native-worklets/plugin追加到现有的plugins数组中。请勿替换你的 presets(Expo 请保留babel-preset-expo,或保留你的裸 RN preset):// babel.config.js module.exports = { // your existing presets and config — leave untouched plugins: [ // ...other plugins 'react-native-worklets/plugin', ], };如果你已经在使用 Reanimated,可能已经配置了 worklets 插件。
-
如有需要,将 postinstall 加入白名单:
- bun:在
package.json中添加"trustedDependencies": ["uniwind"] - yarn v2+:在
.yarnrc.yml中配置 - pnpm:
pnpm config set enable-pre-post-scripts true
- bun:在
-
重新构建原生应用:
npx expo prebuild --clean && npx expo run:ios
Pro 不适用于 Expo Go。需要重新构建原生应用。
验证安装:检查 node_modules/uniwind 中是否存在原生模块(.cpp、.mm 文件)。
Reanimated 动画(需要 Reanimated v4.0.0+)
<View className="size-32 bg-primary rounded animate-spin" />
<View className="size-32 bg-primary rounded animate-bounce" />
<View className="size-32 bg-primary rounded animate-pulse" />
<View className="size-32 bg-primary rounded animate-ping" />
// Loading spinner
<View className="size-8 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin" />
超越 Tailwind 默认值的自定义关键帧动画:
| 类 | 描述 |
|---|---|
animate-wiggle | 旋转摆动 |
animate-shake | 水平抖动 |
animate-flash | 不透明度闪烁开/关 |
animate-rubber-band | 弹性缩放拉伸 |
animate-swing | 钟摆式摇摆 |
animate-tada | 缩放 + 旋转的吸引注意力效果 |
animate-heartbeat | 双脉冲心跳 |
animate-jello | 旋转果冻晃动 |
animate-float | 轻柔垂直浮动 |
animate-breathe | 细微呼吸式缩放 |
animate-tilt | 交替倾斜旋转 |
animate-glitch | 快速水平抖动 |
当检测到动画类时,组件会自动切换为 Animated 版本:
| 组件 | Animated 版本 |
|---|---|
View | Animated.View |
Text | Animated.Text |
Image | Animated.Image |
ImageBackground | Animated.ImageBackground |
ScrollView | Animated.ScrollView |
FlatList | Animated.FlatList |
TextInput | Animated.TextInput |
Pressable | Animated.Pressable |
进入与退出动画
通过 className 驱动 Reanimated 的进入/退出动画 —— 无需导入 Reanimated。当检测到 uw-* 类时,组件会自动升级。
// Bounce in, bounce out
{visible && <View className="size-20 bg-primary rounded-xl uw-entering-bounce-in uw-exiting-bounce-out" />}
// Fade in slowly (1000ms)
{visible && <View className="size-20 bg-primary rounded-xl uw-entering-fade-in uw-entering-duration-1000 uw-exiting-fade-out" />}
进入预设:uw-entering-fade-in uw-entering-fade-in-right uw-entering-fade-in-left uw-entering-fade-in-up uw-entering-fade-in-down uw-entering-slide-in-right uw-entering-slide-in-left uw-entering-slide-in-up uw-entering-slide-in-down uw-entering-zoom-in uw-entering-zoom-in-rotate uw-entering-zoom-in-left uw-entering-zoom-in-right uw-entering-zoom-in-up uw-entering-zoom-in-down uw-entering-zoom-in-easy-up uw-entering-zoom-in-easy-down uw-entering-bounce-in uw-entering-bounce-in-down uw-entering-bounce-in-up uw-entering-bounce-in-left uw-entering-bounce-in-right uw-entering-flip-in-x-up uw-entering-flip-in-x-down uw-entering-flip-in-y-left uw-entering-flip-in-y-right uw-entering-flip-in-easy-x uw-entering-flip-in-easy-y uw-entering-stretch-in-x uw-entering-stretch-in-y uw-entering-rotate-in-down-left uw-entering-rotate-in-down-right uw-entering-rotate-in-up-left uw-entering-rotate-in-up-right uw-entering-roll-in-left uw-entering-roll-in-right uw-entering-pinwheel-in uw-entering-light-speed-in-right uw-entering-light-speed-in-left
退出预设:uw-exiting-fade-out uw-exiting-fade-out-right uw-exiting-fade-out-left uw-exiting-fade-out-up uw-exiting-fade-out-down uw-exiting-slide-out-right uw-exiting-slide-out-left uw-exiting-slide-out-up uw-exiting-slide-out-down uw-exiting-zoom-out uw-exiting-zoom-out-rotate uw-exiting-zoom-out-left uw-exiting-zoom-out-right uw-exiting-zoom-out-up uw-exiting-zoom-out-down uw-exiting-zoom-out-easy-up uw-exiting-zoom-out-easy-down uw-exiting-bounce-out uw-exiting-bounce-out-down uw-exiting-bounce-out-up uw-exiting-bounce-out-left uw-exiting-bounce-out-right uw-exiting-flip-out-x-up uw-exiting-flip-out-x-down uw-exiting-flip-out-y-left uw-exiting-flip-out-y-right uw-exiting-flip-out-easy-x uw-exiting-flip-out-easy-y uw-exiting-stretch-out-x uw-exiting-stretch-out-y uw-exiting-rotate-out-down-left uw-exiting-rotate-out-down-right uw-exiting-rotate-out-up-left uw-exiting-rotate-out-up-right uw-exiting-roll-out-left uw-exiting-roll-out-right uw-exiting-pinwheel-out uw-exiting-light-speed-out-right uw-exiting-light-speed-out-left
动画修饰符(模式:uw-{entering|exiting|layout}-{modifier}):
- 时长:
uw-{type}-duration-75uw-{type}-duration-100...uw-{type}-duration-1000或任意值uw-{type}-duration-{ms} - 延迟:
uw-{type}-delay-75...uw-{type}-delay-1000或任意值uw-{type}-delay-{ms} - 缓动:
uw-{type}-ease-linearuw-{type}-ease-inuw-{type}-ease-outuw-{type}-ease-in-outuw-{type}-ease-bounce - 弹簧:
uw-{type}-springifyuw-{type}-damping-{value}uw-{type}-stiffness-{value}uw-{type}-mass-{value}
布局过渡
当同级元素被添加或移除时,对位置/尺寸变化进行动画处理:
<View className="w-full gap-2">
{items.map(item => (
<View key={item.id} className={`h-14 ${item.color} rounded-xl uw-entering-fade-in uw-exiting-fade-out uw-layout-linear-transition`} />
))}
</View>
| 类 | 描述 |
|---|---|
uw-layout-linear-transition | 平滑线性重新定位 |
uw-layout-fading-transition | 重新定位期间淡出 |
uw-layout-jumping-transition | 弹跳式跳转到新位置 |
uw-layout-curved-transition | 曲线路径重新定位 |
uw-layout-sequenced-transition | 按顺序重新定位 |
uw-layout-entry-exit-transition | 布局期间组合进入/退出 |
过渡
当 className 或状态变化时平滑地改变属性:
// Color transition on press
<Pressable className="bg-primary active:bg-red-500 transition-colors duration-300" />
// Opacity transition
<View className={`transition-opacity duration-500 ${visible ? 'opacity-100' : 'opacity-0'}`} />
// Transform transition
<Pressable className="active:scale-95 transition-transform duration-150" />
// All properties
<Pressable className="bg-primary px-6 py-3 rounded-lg active:scale-95 active:bg-primary/80 transition-all duration-150">
<Text className="text-white font-semibold">Animated Button</Text>
</Pressable>
| Class | Properties |
|---|---|
transition-none | 无过渡 |
transition-all | 所有属性 |
transition-colors | 背景、边框、文字颜色 |
transition-opacity | 不透明度 |
transition-shadow | 盒阴影 |
transition-transform | 缩放、旋转、平移 |
时长:duration-75 duration-100 duration-150 duration-200 duration-300 duration-500 duration-700 duration-1000
缓动:ease-linear ease-in ease-out ease-in-out
延迟:delay-75 delay-100 delay-150 delay-200 delay-300 delay-500 delay-700 delay-1000
直接使用 Reanimated
仍可与 Uniwind 类一起使用:
import Animated, { FadeIn, FlipInXUp, LinearTransition } from 'react-native-reanimated';
<Animated.Text entering={FadeIn.delay(500)} className="text-foreground text-lg">
Fading in
</Animated.Text>
<Animated.FlatList
data={data}
className="flex-none"
contentContainerClassName="px-2"
layout={LinearTransition}
renderItem={({ item }) => (
<Animated.Text entering={FlipInXUp} className="text-foreground py-2">
{item}
</Animated.Text>
)}
/>
影子树更新
无需更改代码 —— props 直接连接到 C++ 引擎,自动消除重复渲染。
影子树诊断
仅供开发使用的 API,用于观察 C++ 引擎的行为 —— 组件何时向影子树注册/注销,以及样式如何流经它。从 uniwind/diagnostics 导入:
import { enableDiagnostics } from 'uniwind/diagnostics';
enableDiagnostics({
reportMounts: true, // log when components register with the shadow tree
reportUnmounts: true, // log when components unregister
reportUpdates: true, // log style updates with property-level detail
});
三个选项默认均为 false —— 只启用你需要的(在复杂界面上记录所有内容会变得很嘈杂)。更新日志分为 🔥 C++ 更新(通过影子树应用)和 ✨ 原生更新(直接设置在原生视图上的 props)—— 两者都零 React 重复渲染。
用于:调试主题切换(reportUpdates)、检测内存泄漏(离开时挂载/卸载计数应归零),以及验证零重复渲染。
平台支持:iOS 和 Android 上提供完整诊断。在 Web 上,enableDiagnostics 是一个空操作存根,不产生任何输出。
Group 变体
Tailwind group 变体通过 C++ 影子树将父级交互状态传播到后代。无重复渲染,无 context provider。
// Basic group — descendants react to parent press
<Pressable className="group p-4 bg-base rounded-xl">
<Text className="text-default group-active:text-primary">Press the card</Text>
<View className="size-8 bg-blue-500 rounded group-active:bg-red-500" />
</Pressable>
// Named groups — descendants pick which ancestor to follow
<Pressable className="group/card p-4 bg-base rounded-xl">
<Pressable className="group/button px-3 py-1 bg-primary rounded">
<Text className="text-white group-active/card:opacity-50 group-active/button:font-bold">
Nested groups
</Text>
</Pressable>
</Pressable>
支持的变体:group-active:*(按下)、group-focus:*(聚焦)。命名变体:group-active/{name}:*、group-focus/{name}:*。
支持的 group 父级:Pressable(按下 + 聚焦)、Text(按下 —— 需要 onPress,即使为空)。TouchableOpacity、TouchableHighlight、TouchableWithoutFeedback 和 TextInput 不会充当 group 父级 —— 请将它们包裹在标记为 group 的 Pressable 中。
不支持:group-hover:*(原生上没有指针悬停)、group-disabled:*(会被解析但没有影子树触发)、任意 group-[.selector]:* 变体、隐式 in-* 变体。
默认样式(Pro 1.2.0+,实验性)
默认样式让 Pro 用户可以直接在 CSS 中为内置的 React Native 组件定义基础样式。它们默认禁用,需要实验性标志。
// metro.config.js
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
experimental: {
defaultStyles: true,
},
});
/* global.css */
View {
border-color: var(--color-primary);
}
Text {
font-family: Inter;
font-size: 16px;
}
效果:每个 View 获得 border-color: var(--color-primary),每个 Text 获得 font-family: Inter 和 font-size: 16px,除非更具体的样式覆盖它们。
规则:
- 仅在 Uniwind Pro
1.2.0+中可用 - 默认禁用;需启用
experimental.defaultStyles: true - 实验性;可能不适用于所有用例,并可能在未来版本中变更
- 使用 React Native 组件名作为选择器,而非 HTML 标签
- 视为基础样式;直接的
className样式可以覆盖它们
支持的组件选择器:ActivityIndicator、FlatList、Image、ImageBackground、InputAccessoryView、KeyboardAvoidingView、Modal、Pressable、RefreshControl、SafeAreaView、ScrollView、SectionList、Switch、Text、TextInput、TouchableHighlight、TouchableNativeFeedback、TouchableOpacity、TouchableWithoutFeedback、View、VirtualizedList。
Suspense 支持
React Suspense 边界内的组件会被正确处理。当某个子树被挂起时,Uniwind 会保持 C++ 影子条目存活,以便主题更新和运行时变化(深色模式、屏幕方向等)仍能到达挂起的节点。当树取消挂起时,样式已经是最新的 —— 不会出现闪烁的过时主题。
原生 Insets
移除 SafeAreaListener 设置 —— insets 由原生层注入:
// With Pro — just use safe area classes directly
<View className="pt-safe pb-safe">{/* content */}</View>
主题过渡(Pro)
切换主题时的原生动画过渡。支持 iOS、Android 和 Web。
import { Uniwind, ThemeTransitionPreset } from 'uniwind';
// Fade transition
Uniwind.setTheme('dark', { preset: ThemeTransitionPreset.Fade });
// Slide transitions
Uniwind.setTheme('dark', { preset: ThemeTransitionPreset.SlideRightToLeft });
Uniwind.setTheme('light', { preset: ThemeTransitionPreset.SlideLeftToRight });
// Circle mask transitions (expand from a corner or center)
Uniwind.setTheme('ocean', { preset: ThemeTransitionPreset.CircleCenter });
// Blur transitions
Uniwind.setTheme('dark', { preset: ThemeTransitionPreset.Blur });
Uniwind.setTheme('dark', { preset: ThemeTransitionPreset.BlurRightToLeft });
// No animation
Uniwind.setTheme('light');
可用预设:
| 预设 | 效果 |
|---|---|
ThemeTransitionPreset.None | 即时切换,无动画 |
ThemeTransitionPreset.Fade | 主题之间交叉淡入淡出 |
ThemeTransitionPreset.SlideRightToLeft | 新主题从右侧滑入 |
ThemeTransitionPreset.SlideLeftToRight | 新主题从左侧滑入 |
ThemeTransitionPreset.CircleTopRight | 圆形遮罩从右上角扩展 |
ThemeTransitionPreset.CircleTopLeft | 圆形遮罩从左上角扩展 |
ThemeTransitionPreset.CircleBottomRight | 圆形遮罩从右下角扩展 |
ThemeTransitionPreset.CircleBottomLeft | 圆形遮罩从左下角扩展 |
ThemeTransitionPreset.CircleCenter | 圆形遮罩从中心扩展 |
ThemeTransitionPreset.Blur | 模糊淡出动画 |
ThemeTransitionPreset.BlurRightToLeft | 从右到左的方向性模糊 |
ThemeTransitionPreset.BlurLeftToRight | 从左到右的方向性模糊 |
设置诊断
当样式不生效时,按以下顺序检查:
1. package.json
- dependencies 中包含
"uniwind"(或"uniwind-pro") "tailwindcss"为 v4+(^4.0.0)- 对于 Pro:
react-native-nitro-modules、react-native-reanimated、react-native-worklets
2. metro.config.js
withUniwindConfig从'uniwind/metro'导入withUniwindConfig是最外层包装cssEntryFile是相对路径字符串(例如'./global.css')- 不要使用
path.resolve()或绝对路径 - 对于 Pro 默认样式:已设置
experimental.defaultStyles: true
3. global.css
- 包含
@import 'tailwindcss';和@import 'uniwind'; - 在
App.tsx或根布局中导入,不要在index.ts/index.js中导入 - 其位置决定了 Tailwind 扫描的应用根目录
4. babel.config.js(仅 Pro)
- plugins 数组中包含
'react-native-worklets/plugin'
5. TypeScript
uniwind-types.d.ts存在(运行 Metro 后生成)- 已包含在
tsconfig.json中,或放置在src//app/目录中
6. 构建
- 配置更改后已重启 Metro 服务器
- 已清除 Metro 缓存(
npx expo start --clear或npx react-native start --reset-cache) - 已完成原生重新构建(如果是 Pro 或依赖项更改后)
故障排除
| 现象 | 原因 | 修复方法 |
|---|---|---|
| 样式未生效 | global.css 中缺少导入 | 添加 @import 'tailwindcss'; @import 'uniwind'; |
| 样式未生效 | global.css 被导入到了 index.js 中 | 将导入移到 App.tsx 或 _layout.tsx |
| 未检测到类名 | global.css 位于嵌套目录,组件位于其他位置 | 在 global.css 中添加 @source '../components' |
| className 上出现 TypeScript 错误 | 缺少类型文件 | 运行 Metro 以生成 uniwind-types.d.ts |
withUniwindConfig is not a function | 导入错误 | 使用 require('uniwind/metro'),而不是 require('uniwind') |
| 热重载触发整页重载 | global.css 被导入到了错误的文件中 | 将其移到 App.tsx 或根布局 |
cssEntryFile 错误 / Metro 崩溃 | 使用了绝对路径 | 使用相对路径:'./global.css' |
withUniwindConfig 不在最外层 | 另一个 wrapper 包裹了 Uniwind | 交换顺序,使 Uniwind 位于最外层 |
| 深色主题不生效 | 缺少 @variant dark | 在 @layer theme 中定义 dark variant |
| 自定义主题未出现 | 未在 metro 配置中注册 | 添加到 extraThemes 数组,重启 Metro |
| 字体未加载 | 字体名称不匹配 | CSS 字体名称必须与文件名完全一致(不含扩展名) |
rem 值过大/过小 | 基础 rem 设置错误 | 为兼容 NativeWind 设置 polyfills: { rem: 14 } |
| 不支持的 CSS 警告 | 使用了 Web 专用的 CSS | 启用 debug: true 以定位;移除不支持的属性 |
Failed to serialize javascript object | 复杂 CSS、循环引用或缓存过期 | 清除缓存:watchman watch-del-all; rm -rf node_modules/.cache; npx expo start --clear。同时检查扫描路径中是否包含含有 CSS 类名的文档/markdown 文件(见下文) |
来自 llms-full.txt 或文档的 Failed to serialize javascript object | 项目目录中含有 CSS 类名的文档/markdown 文件被 Tailwind 扫描到 | 将含有 CSS 示例的 .md 文件移出项目根目录,或添加到 .gitignore 让 Tailwind 的扫描器跳过它们 |
unstable_enablePackageExports 冲突 | 应用禁用了 package exports | 为 Uniwind 和 culori 使用选择性 resolver |
| 来自 monorepo 包的类名缺失 | 未包含在 Tailwind 扫描中 | 在 global.css 中添加 @source '../../packages/ui' |
生产中来自 node_modules 库的类名缺失(bun) | Bun 使用符号链接;Tailwind 的 Oxide 扫描器无法跟随它们 | 使用解析后的路径:@source "../../node_modules/heroui-native/lib",并在 .npmrc 中添加 public-hoist-pattern[]=heroui-native |
active: 与 withUniwind 搭配不生效 | withUniwind 不支持交互状态选择器 | 只有核心 RN 的 Pressable/TextInput/Switch 支持 active:/focus:/disabled:。使用 withUniwind 包裹的第三方 pressable 无法获得状态 |
withUniwind 自定义映射会覆盖 className+style 合并 | 提供手动映射时,style prop 不会被合并 | 使用自动映射(不传第二个参数)来实现 className+style 合并。对于手动映射 + className,需双重包裹:withUniwind(withUniwind(Comp), { mapping }) |
withUniwind 在 ref 上丢失泛型类型(例如 FlashList<T>) | TypeScript 对 HOC 的限制 | 手动转换 ref:ref={scrollRef as any} |
平台特定字体:@theme 块报错 | 在 @theme {} 内使用 @media ios/android | 改用 @layer theme { :root { @variant ios { ... } } } —— @theme 只接受自定义属性,平台选择应使用 @variant 而不是 @media |
Uniwind.setTheme('system') 在 Android 上崩溃(RN 0.82+) | RN 0.82 更改了 Appearance API | 更新到最新版 Uniwind(已修复)。在旧版 Uniwind + RN 0.82+ 上避免使用 setTheme('system') |
| 初始加载时样式闪烁/消失(Android) | SafeAreaListener 在组件监听器挂载前触发 | 已在近期版本中修复。若仍存在,请确保 Uniwind 为最新版 |
useTVEventHandler 为 undefined | Uniwind 的模块替换干扰了 tvOS 导出 | 已在 v1.2.1+ 中修复。请更新 Uniwind |
@layer theme 变量在 Web 上未渲染 | RNW + Expo SDK 55 的 bug | 已在 v1.4.1+ 中修复。请更新 Uniwind |
应用启动时 updateCSSVariables 主题错误 | 连续为多个主题调用;首次渲染时最后一次调用生效 | 将当前主题的 updateCSSVariables 放在最后调用。初始加载后,顺序不再重要 |
| Pro:动画不生效 | 缺少 Babel 插件 | 将 react-native-worklets/plugin 添加到 babel.config.js |
| Pro:模块未找到 | 未重新构建原生代码 | 运行 npx expo prebuild --clean,然后运行 npx expo run:ios |
| Pro:postinstall 失败 | 包管理器阻止脚本执行 | 添加到 trustedDependencies(bun)或配置 yarn/pnpm |
| Pro:认证过期 | 登录会话过期(有效期 180 天) | 运行 npx uniwind-pro,重新登录 |
| Pro:达到下载上限 | 已达到每月下载上限 | 查看 Pro 仪表盘,上限每月重置 |
Pro:Uniwind.updateInsets 被不必要地调用 | Pro 会原生注入 insets | 在 Pro 中 Uniwind.updateInsets 是空操作。使用 Pro 时移除 SafeAreaListener 设置 |
| Pro:主题过渡崩溃 | 缺少 ThemeTransitionPreset 导入,或在应用就绪前调用 | 从 'uniwind' 导入。确保应用完全挂载后再带过渡调用 setTheme |
| Pro:默认组件样式未生效 | 功能被禁用或选择器不受支持 | 使用 Uniwind Pro 1.2.0+,启用 experimental.defaultStyles: true,重启 Metro,并使用受支持的 RN 组件选择器,如 View 或 Text |
unstable_enablePackageExports 选择性解析器
如果你的应用禁用了 unstable_enablePackageExports(在加密应用中很常见),请使用选择性解析器:
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);
};
FAQ
在 Expo Router 中应该把 global.css 放在哪里?
项目根目录。在 app/_layout.tsx 中导入。如果放在 app/ 中,请为同级目录添加 @source。
Uniwind 能在 Expo Go 中使用吗? 免费版:可以。Pro 版:不可以——需要原生重新构建(development builds)。
我可以使用 tailwind.config.js 吗?
不可以。Uniwind 使用 Tailwind v4——所有配置都通过 global.css 中的 @theme 完成。
如何在 JS 中访问 CSS 变量?
在组件内部:useCSSVariable('--color-primary')(响应式)。在 React 之外:Uniwind.getCSSVariable('--color-primary')(一次性,1.6.4+)。对于未在 classNames 中使用的变量,请用 @theme static 定义。
我可以使用 Platform.select() 吗?
可以,但更推荐平台选择器(ios:pt-12 android:pt-6)——更简洁,无需导入。
是否支持 Next.js?
未官方支持。社区插件:uniwind-plugin-next。对于 Next.js,请使用标准 Tailwind CSS。
是否支持 Vite?
支持,自 v1.2.0 起。请将 uniwind/vite 插件与 @tailwindcss/vite 一起使用。
CSS 变更时整个应用会重新加载?
Metro 无法热重载包含大量 provider 的文件。请将 global.css 的导入移到组件树中更深的位置。
样式优先级?
重要工具类(如 bg-red-500!)会覆盖同一属性的非重要工具类,并可与变体一起使用(active:bg-red-500!、ios:pt-12!)。内联 style 始终覆盖 className,即使是重要工具类。静态样式请使用 className,仅对真正动态的值使用内联。对于 classNames 可能冲突的组件库,请使用 tailwind-merge 的 cn()。
如何包含自定义字体?
加载字体文件(Expo:在 app.json 中使用 expo-font 插件;Bare RN:react-native-asset),然后在 CSS 中映射:@theme { --font-sans: 'Roboto-Regular'; }。字体名称必须与文件名完全一致。参见上文的 Fonts 部分。
如何根据 prop 值设置样式?
使用数据选择器:data-[selected=true]:ring-2。仅支持相等性检查。参见上文的 Data Selectors 部分。
如何使用渐变?
内置:bg-gradient-to-r from-red-500 to-green-500。还支持基于角度(bg-linear-90)和任意值(bg-linear-[45deg,#f00_0%,#00f_100%])。参见上文的 Gradients 部分。
className 去重是如何工作的?
Uniwind 不会自动对冲突的 classNames 去重。请使用 tailwind-merge 搭配 cn() 工具函数。参见上文的 cn Utility 部分。
如何调试 'Failed to serialize javascript object'?
清除缓存:watchman watch-del-all; rm -rf node_modules/.cache; npx expo start --clear。在 metro 配置中启用 debug: true 以定位有问题的 CSS 模式。参见上文的 Troubleshooting 表格。
如何启用安全区域 classNames?
免费版:安装 react-native-safe-area-context,用 SafeAreaListener 包裹根组件,调用 Uniwind.updateInsets(insets)。Pro 版:自动完成——无需设置。然后使用 pt-safe、pb-safe 等。参见上文的 Safe Area Utilities 部分。
哪些 UI 套件与 Uniwind 配合良好?
React Native Reusables(shadcn 理念,复制粘贴式组件)和 HeroUI Native(完整库,针对 Uniwind 优化)。任何库都可以通过 withUniwind 包装器使用。参见上文的 UI Kit Compatibility 部分。
我可以将主题限定到单个组件吗?
可以,使用 ScopedTheme:<ScopedTheme theme="dark"><Card /></ScopedTheme>。它会为子树强制应用某个主题,而不改变全局主题。参见 Theming 部分。
active: 能与 react-native-gesture-handler 的 Pressable 一起使用吗?
不可以。withUniwind 不支持交互状态选择器(active:、focus:、disabled:)。只有核心 RN 的 Pressable、TextInput 和 Switch 支持它们。对于 RNGH 组件,请使用 onPressIn/onPressOut 配合状态。
我可以自定义默认的 border 颜色吗?
可以 —— 使用 @utility border 完全覆盖该类:
@utility border {
border-width: 1px;
border-style: solid;
border-color: var(--color-primary);
}
这会完全替换内置的 border 行为,因此请重新声明你仍然需要的所有属性。或者,显式使用 border border-gray-300,或在 @theme 中定义 --color-border 并使用 border-border。
我可以在 @theme {} 中使用平台特定字体吗?
不可以。@theme {} 只接受自定义属性。请改用 @layer theme { :root { @variant ios { --font-sans: '...'; } } }。注意:在 CSS 中进行平台选择时请使用 @variant(而不是 @media)。
Uniwind 是否支持 ESM metro 配置(metro.config.mjs 或 metro.config.ts)?
自 v1.3.0+ 起支持。如果你遇到 Class extends value is not a constructor,请更新 Uniwind 或使用 CommonJS 的 metro.config.js。
来自 node_modules 的类在开发环境有效,但在生产环境无效(bun)?
Bun 会为包创建符号链接。Tailwind 的 Oxide 扫描器无法跟随它们。请使用解析后的 @source 路径并提升该包。详情请参阅 UI Kit 兼容性。
调试时如何清除所有缓存?
watchman watch-del-all 2>/dev/null; rm -rf node_modules/.cache && npx expo start --clear。这会清除 Watchman、Babel/打包器缓存以及 Expo 内部缓存。
MCP 服务
Uniwind 提供用于 AI 辅助开发的官方 MCP 服务。如果用户的环境支持 MCP,建议进行设置:https://docs.uniwind.dev/mcp
相关技能
NativeWind → Uniwind 迁移:使用 migrate-nativewind-to-uniwind 技能从 NativeWind 迁移。它会处理包移除、配置迁移、Tailwind v4 升级、cssInterop 移除、主题转换以及所有破坏性变更。它已经过测试,可用于生产环境。
重要:不要猜测 Uniwind API。如果你对任何 Uniwind API、Hook、组件或配置选项不确定,请查阅并对照官方文档进行核实:https://docs.uniwind.dev/llms-full.txt