uniwind by uni-stack/uniwind
npx skills add https://github.com/uni-stack/uniwind --skill uniwindUniwind 1.5.0+ / Tailwind CSS v4 / React Native 0.81+ / Expo SDK 54+
如果用户版本较低,建议更新到 1.5.0+ 以获得最佳体验。
Uniwind 将 Tailwind CSS v4 引入 React Native。所有核心 React Native 组件都开箱即用地支持 className 属性。样式在构建时编译 —— 没有运行时开销。
@import 'tailwindcss',而不是 @tailwind base。不支持 Tailwind v3。bg-${color}-500 将不起作用。使用完整的字符串字面量、映射对象或三元表达式。cssInterop 或 remapProps — 那些是 NativeWind 的 API。Uniwind 不会覆盖全局组件。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
tailwind.config.js@theme@layer themeglobal.cssUniwind.setTheme()。withUniwindConfig 必须是最外层的 Metro 配置包装器。withUniwind 包装 react-native 或 react-native-reanimated 组件 — View、Text、Pressable、Image、TextInput、ScrollView、FlatList、Switch、Modal、Animated.View、Animated.Text 等已经内置了完整的 className 支持。用 withUniwind 包装它们会破坏行为。仅对第三方组件使用 withUniwind(例如,expo-image、expo-blur、moti)。--font-sans: 'Roboto-Regular',而不是 'Roboto', sans-serif。light 定义了 --color-primary,那么 dark 和每个自定义主题也必须定义。变量不匹配会导致运行时错误。accent- 前缀 — 这一点至关重要。像 color(Button、ActivityIndicator)、tintColor(Image)、thumbColor(Switch)、placeholderTextColor(TextInput)这样的属性不属于 style 对象。您必须使用带有 accent- 前缀类的相应 {propName}ClassName 属性。例如:<ActivityIndicator colorClassName="accent-blue-500" />,而不是 <ActivityIndicator className="text-blue-500" />。常规的 Tailwind 颜色类(如 text-blue-500)仅适用于 className(它映射到 style)。对于非样式颜色属性,始终使用 accent-。polyfills: { rem: 14 }。cssEntryFile 必须是相对路径字符串 — 使用 './global.css',而不是 path.resolve(__dirname, 'global.css')。cn() 去重 — Uniwind 不自动去重。如果一个自定义 CSS 类(.card { padding: 16px })和一个 Tailwind 工具类(p-6)设置了相同的属性,两者都会应用,结果不可预测。当存在重叠时,始终用 cn('card', 'p-6') 包装。# 或其他包管理器
bun install uniwind tailwindcss
需要 Tailwind CSS v4+。
创建一个 CSS 入口文件:
@import 'tailwindcss';
@import 'uniwind';
在您的 App 组件(例如,App.tsx 或 app/_layout.tsx)中导入,不要在 index.ts/index.js 中导入 —— 在那里导入会破坏热重载:
// app/_layout.tsx 或 App.tsx
import './global.css';
包含 global.css 的目录是应用根目录 —— Tailwind 从此目录开始扫描 classNames。
const { getDefaultConfig } = require('expo/metro-config');
// 裸 RN:const { getDefaultConfig } = require('@react-native/metro-config');
const { withUniwindConfig } = require('uniwind/metro');
const config = getDefaultConfig(__dirname);
// withUniwindConfig 必须是 OUTERMOST 包装器
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css', // 必需 —— 相对于项目根目录的路径
polyfills: { rem: 16 }, // 可选 —— 基础 rem 值(默认 16)
extraThemes: ['ocean', 'sunset'], // 可选 —— light/dark 之外的自定义主题
dtsFile: './uniwind-types.d.ts', // 可选 —— TypeScript 类型输出路径
debug: true, // 可选 —— 在开发中记录不支持的 CSS
isTV: false, // 可选 —— 启用 TV 平台支持
});
对于大多数流程,保持默认值,只提供 cssEntryFile。
包装器顺序 —— Uniwind 必须包装其他所有内容:
// 正确
module.exports = withUniwindConfig(withOtherConfig(config, opts), { cssEntryFile: './global.css' });
// 错误 —— Uniwind 不是最外层
module.exports = withOtherConfig(withUniwindConfig(config, { cssEntryFile: './global.css' }), opts);
如果用户有 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',
}),
],
});
Uniwind 在运行 Metro 后会自动生成一个 .d.ts 文件(默认:./uniwind-types.d.ts)。将其放在 src/ 或 app/ 中以自动包含,或添加到 tsconfig.json:
{ "include": ["./uniwind-types.d.ts"] }
如果用户有一些与 classNames 相关的 typescript 错误,只需运行 metro 服务器来构建 d.ts 文件。
project/
├── app/_layout.tsx ← 在此处导入 '../global.css'
├── components/
├── global.css ← 项目根目录(最佳位置)
└── metro.config.js ← cssEntryFile: './global.css'
如果 global.css 在 app/ 目录中,为同级目录添加 @source:
@import 'tailwindcss';
@import 'uniwind';
@source '../components';
{
"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"]
}
为 CSS 入口文件目录之外的包在 global.css 中添加 @source 指令:
@import 'tailwindcss';
@import 'uniwind';
@source "../../packages/ui/src";
@source "../../packages/shared/src";
对于包含 Uniwind 类的 node_modules 包(例如,共享的 UI 库)也需要。
所有核心 React Native 组件都开箱即用地支持 className。有些组件还有用于子样式(如 contentContainerClassName)和非样式颜色属性(需要 accent- 前缀)的额外 className 属性。
图例:标记有 ⚡ 的属性需要 accent- 前缀。括号中的属性是平台特定的。
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
selectionColorClassName | selectionColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
支持 active:、disabled:、focus: 状态选择器。
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
tintColorClassName | tintColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
cursorColorClassName | cursorColor | ⚡ accent- |
selectionColorClassName | selectionColor | ⚡ accent- |
placeholderTextColorClassName | placeholderTextColor | ⚡ accent- |
selectionHandleColorClassName | selectionHandleColor | ⚡ accent- |
underlineColorAndroidClassName | underlineColorAndroid (Android) | ⚡ accent- |
支持 focus:、active:、disabled: 状态选择器。
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
columnWrapperClassName | columnWrapperStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
thumbColorClassName | thumbColor | ⚡ accent- |
trackColorOnClassName | trackColor.true (on) | ⚡ accent- |
trackColorOffClassName | trackColor.false (off) | ⚡ accent- |
ios_backgroundColorClassName | ios_backgroundColor (iOS) | ⚡ accent- |
注意:Switch 不支持 className(类型中为 className?: never)。仅使用上述特定颜色的 className 属性。支持 disabled: 状态选择器。
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
colorClassName | color | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
colorClassName | color | ⚡ accent- |
注意:Button 不支持 className(RN Button 上没有 style 属性)。
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
backdropColorClassName | backdropColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
colorsClassName | colors (Android) | ⚡ accent- |
tintColorClassName | tintColor (iOS) | ⚡ accent- |
titleColorClassName | titleColor (iOS) | ⚡ accent- |
progressBackgroundColorClassName | progressBackgroundColor (Android) | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
imageClassName | imageStyle | — |
tintColorClassName | tintColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
backgroundColorClassName | backgroundColor | ⚡ accent- |
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
underlayColorClassName | underlayColor | ⚡ accent- |
支持 active:、disabled: 状态选择器。
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
支持 active:、disabled: 状态选择器。
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
支持 active:、disabled: 状态选择器。
| 属性 | 映射到 | 前缀 |
|---|---|---|
className | style | — |
支持 active:、disabled: 状态选择器。
import { View, Text, Pressable, TextInput, ScrollView, FlatList, Switch, Image, ActivityIndicator, Modal, RefreshControl, Button } from 'react-native';
// View — 基础布局
<View className="flex-1 bg-background p-4">
<Text className="text-foreground text-lg font-bold">标题</Text>
</View>
// Pressable — 带有按下/焦点状态
<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">按我</Text>
</Pressable>
// TextInput — 带有焦点状态和 accent- 颜色属性
<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="输入文本..."
/>
// ScrollView — 带有内容容器
<ScrollView className="flex-1" contentContainerClassName="p-4 gap-4">
{/* 内容 */}
</ScrollView>
// FlatList — 带有所有子样式属性
<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 — 不支持 className,仅使用特定颜色属性
<Switch
thumbColorClassName="accent-white"
trackColorOnClassName="accent-primary"
trackColorOffClassName="accent-gray-300 dark:accent-gray-700"
ios_backgroundColorClassName="accent-gray-200"
/>
// Image — 色调颜色
<Image className="w-6 h-6" tintColorClassName="accent-primary" source={icon} />
// ActivityIndicator
<ActivityIndicator className="m-4" colorClassName="accent-primary" size="large" />
// Button — 仅 colorClassName(无 className)
<Button colorClassName="accent-primary" title="提交" onPress={handleSubmit} />
// Modal — 背景颜色
<Modal className="flex-1" backdropColorClassName="accent-black/50">
{/* 内容 */}
</Modal>
// RefreshControl — 平台特定颜色属性
<RefreshControl
className="p-4"
tintColorClassName="accent-primary"
titleColorClassName="accent-gray-500"
colorsClassName="accent-primary"
progressBackgroundColorClassName="accent-white dark:accent-gray-800"
/>
// ImageBackground — 独立的图像样式
<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">叠加层</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="输入..." />
</KeyboardAvoidingView>
// InputAccessoryView
<InputAccessoryView
className="p-4 border-t border-gray-300"
backgroundColorClassName="accent-white dark:accent-gray-800"
>
<Button title="完成" onPress={dismissKeyboard} />
</InputAccessoryView>
// TouchableHighlight — 按下时底层颜色
<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">按我</Text>
</TouchableHighlight>
React Native 组件具有像 color、tintColor、thumbColor 这样的属性,它们不属于 style 对象。要通过 Tailwind 类设置这些属性,请使用带有相应 {propName}ClassName 属性的 accent- 前缀:
// color 属性 → 带有 accent- 前缀的 colorClassName
<ActivityIndicator colorClassName="accent-blue-500 dark:accent-blue-400" />
<Button colorClassName="accent-primary" title="提交" />
// tintColor 属性 → 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)。这些需要单独的带有 accent- 前缀的 {propName}ClassName 属性。没有 accent-,类会解析为样式对象 —— 但这些属性期望的是纯颜色字符串。
// 错误 — className 设置样式,但 ActivityIndicator 的 color 不是样式属性
<ActivityIndicator className="text-blue-500" /> // color 将不会被设置
// 正确 — 使用带有 accent- 前缀的专用 colorClassName 属性
<ActivityIndicator colorClassName="accent-blue-500" /> // color 被设置为 #3b82f6
// 错误 — tintColor 不是 Image 上的样式属性
<Image className="tint-blue-500" source={icon} /> // 不起作用
// 正确
<Image tintColorClassName="accent-blue-500" source={icon} />
在模块级别包装一次,随处使用 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';
// 模块级别包装(切勿在渲染函数内部)
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- 前缀)对于自定义属性映射:
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);
// 然后随处导入: import { Image } from '@/components/styled';
切勿在多个文件中为同一组件调用 withUniwind。
关键:不要对来自 react-native 或 react-native-reanimated 的组件使用 withUniwind。这些已经内置了 className 支持:
// 错误 — View 已经原生支持 className
const StyledView = withUniwind(View); // 不要这样做
const StyledText = withUniwind(Text); // 不要这样做
const StyledAnimatedView = withUniwind(Animated.View); // 不要这样做
// 正确 — 仅包装第三方组件
const StyledExpoImage = withUniwind(ExpoImage); // expo-image
const StyledBlurView = withUniwind(BlurView); // expo-blur
const StyledMotiView = withUniwind(MotiView); // moti
将 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 屏幕选项
<Stack.Navigator screenOptions={{ headerStyle, cardStyle }} />
| 功能 | withUniwind | useResolveClassNames |
|---|---|---|
| 设置 | 每个组件一次 | 每次使用 |
| 性能 | 优化 | 稍慢 |
| 最适合 | 可重用组件 | 一次性、导航配置 |
| 语法 | className="..." | style={...} |
// 已损坏 — 带有变量的模板字面量
<View className={`bg-${color}-500`} />
<Text className={`text-${size}`} />
// 带有完整类名的三元表达式
<View className={isActive ? 'bg-primary' : 'bg-muted'} />
// 映射对象
const colorMap = {
primary: 'bg-blue-500 text-white',
danger: 'bg-red-500 text-white',
ghost: 'bg-transparent text-foreground',
};
<Pressable className={colorMap[variant]} />
// 数组连接用于多个条件
<View className={[
'p-4 rounded-lg',
isActive && 'bg-primary',
isDisabled && 'opacity-50',
].filter(Boolean).join(' ')} />
用于具有变体和复合变体的复杂组件样式:
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">点击</Text>
</Pressable>
Uniwind 不自动去重冲突的 classNames。这意味着如果同一属性出现在多个类中,两者都将被应用,结果不可预测。这在混合自定义 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));
}
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')} />
2. 关键:混合自定义 CSS 类和 Tailwind 工具类 — 如果您的自定义 CSS 类设置了一个属性,而 Tailwind 工具类也设置了该属性,您必须使用 cn() 去重:
/* global.css */
.card {
background-color: white;
border-radius: 12px;
padding: 16px;
}
// 错误 — .card (padding: 16px) 和 p-6 (padding: 24px) 都应用,结果不可预测
<View className="card p-6" />
// 正确 — cn 去重,p-6 覆盖 .card 的 padding
<View className={cn('card', 'p-6')} />
3. tv() 输出与额外类结合 — tv 已经处理自己的变体,但如果您在此基础上添加更多类:
<Pressable className={cn(button({ color: 'primary' }), props.className)} />
<View className="flex-1 p-4 bg-white" /><View className="card-shadow mt-4" />(如果 card-shadow 只设置 box-shadow,而没有任何 Tailwind 类也设置它)立即生效 —— 无需配置:
<View className="bg-white dark:bg-gray-900">
<Text className="text-black dark:text-white">主题化</Text>
</View>
最适合小型应用和原型设计。不适用于自定义主题。
在 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;
}
}
}
// 自动适应当前主题 — 无需 dark: 前缀
<View className="bg-card border border-border p-4 rounded-lg">
<Text className="text-foreground text-lg font-bold">标题</Text>
<Text className="text-muted mt-2">副标题</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;
/* 必须定义与 light/dark 相同的所有变量 */
}
}
}
步骤 2 — 在 metro.config.js 中注册(排除 light/dark — 它们是自动的):
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
extraThemes: ['ocean'],
});
添加主题后重启 Metro。
步骤 3 — 使用:
Uniwind.setTheme('ocean');
import { Uniwind, useUniwind } from 'uniwind';
// 命令式(无重新渲染)
Uniwind.setTheme('dark'); // 强制深色
Uniwind.setTheme('light'); // 强制浅色
Uniwind.setTheme('system'); // 跟随设备(重新启用自适应主题)
Uniwind.setTheme('ocean'); // 自定义主题(必须在 extraThemes 中)
Uniwind.currentTheme; // 当前主题名称
Uniwind.hasAdaptiveThemes; // 如果跟随系统则为 true
// 响应式钩子(更改时重新渲染)
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: '浅色' },
{ name: 'dark', label: '深色' },
{ name: 'system', label: '系统' },
];
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>
);
};
将不同的主题应用于子树而不更改全局主题:
import { ScopedTheme } from 'uniwind';
<View className="gap-3">
<PreviewCard />
<ScopedTheme theme="dark">
<PreviewCard /> {/* 使用深色主题渲染 */}
</ScopedTheme>
<ScopedTheme theme="ocean">
<PreviewCard /> {/* 使用 ocean 主题渲染 */}
</ScopedTheme>
</View>
ScopedTheme 生效(支持嵌套作用域)Uniwind 1.5.0+ / Tailwind CSS v4 / React Native 0.81+ / Expo SDK 54+
If user has lower version, recommend updating to 1.5.0+ for best experience.
Uniwind brings Tailwind CSS v4 to React Native. All core React Native components support the className prop out of the box. Styles are compiled at build time — no runtime overhead.
@import 'tailwindcss' not @tailwind base. Tailwind v3 is not supported.bg-${color}-500 will NOT work. Use complete string literals, mapping objects, or ternaries.cssInterop or remapProps — Those are NativeWind APIs. Uniwind does not override global components.tailwind.config.js — All config goes in global.css via @theme and @layer theme.Uniwind.setTheme() directly.withUniwindConfig must be the outermost Metro config wrapper.react-native or react-native-reanimated components with withUniwind — View, Text, Pressable, Image, TextInput, ScrollView, FlatList, Switch, Modal, , , etc. already have full support built in. Wrapping them with will break behavior. Only use for components (e.g., , , ).--font-sans: 'Roboto-Regular' not 'Roboto', sans-serif.light defines --color-primary, then dark and every custom theme must too. Mismatched variables cause runtime errors.accent- prefix is REQUIRED for non-style color props — This is crucial. Props like color (Button, ActivityIndicator), tintColor (Image), thumbColor (Switch), placeholderTextColor (TextInput) are NOT part of the style object. You MUST use the corresponding {propName}ClassName prop with accent- prefixed classes. Example: <ActivityIndicator colorClassName="accent-blue-500" /> NOT <ActivityIndicator className="text-blue-500" />. Regular Tailwind color classes (like ) only work on (which maps to ). For non-style color props, always use .polyfills: { rem: 14 } in metro config if migrating.cssEntryFile must be a relative path string — Use './global.css' not path.resolve(__dirname, 'global.css').cn() when mixing custom CSS classes and Tailwind — Uniwind does NOT auto-deduplicate. If a custom CSS class (.card { padding: 16px }) and a Tailwind utility (p-6) set the same property, both apply with unpredictable results. Always wrap with cn('card', 'p-6') when there's overlap.# or other package manager
bun install uniwind tailwindcss
Requires Tailwind CSS v4+.
Create a CSS entry file:
@import 'tailwindcss';
@import 'uniwind';
Import in your App component (e.g., App.tsx or app/_layout.tsx), NOT in index.ts/index.js — importing there breaks hot reload:
// app/_layout.tsx or App.tsx
import './global.css';
The directory containing global.css is the app root — Tailwind scans for classNames starting from this directory.
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
});
For most flows, keep defaults, only provide cssEntryFile.
Wrapper order — Uniwind must wrap everything else:
// CORRECT
module.exports = withUniwindConfig(withOtherConfig(config, opts), { cssEntryFile: './global.css' });
// WRONG — Uniwind is NOT outermost
module.exports = withOtherConfig(withUniwindConfig(config, { cssEntryFile: './global.css' }), opts);
If user has storybook setup, add extra vite config:
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',
}),
],
});
Uniwind auto-generates a .d.ts file (default: ./uniwind-types.d.ts) after running Metro. Place it in src/ or app/ for auto-inclusion, or add to tsconfig.json:
{ "include": ["./uniwind-types.d.ts"] }
If user has some typescript errors related to classNames, just run metro server to build the d.ts file.
project/
├── app/_layout.tsx ← import '../global.css' here
├── components/
├── global.css ← project root (best location)
└── metro.config.js ← cssEntryFile: './global.css'
If global.css is in app/ dir, add @source for sibling directories:
@import 'tailwindcss';
@import 'uniwind';
@source '../components';
{
"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"]
}
Add @source directives in global.css for packages outside the CSS entry file's directory:
@import 'tailwindcss';
@import 'uniwind';
@source "../../packages/ui/src";
@source "../../packages/shared/src";
Also needed for node_modules packages that contain Uniwind classes (e.g., shared UI libraries).
All core React Native components support className out of the box. Some have additional className props for sub-styles (like contentContainerClassName) and non-style color props (requiring accent- prefix).
Legend : Props marked with ⚡ require the accent- prefix. Props in parentheses are platform-specific.
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
selectionColorClassName | selectionColor | ⚡ accent- |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
Supports active:, disabled:, focus: state selectors.
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
tintColorClassName | tintColor | ⚡ accent- |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
cursorColorClassName | cursorColor | ⚡ accent- |
selectionColorClassName | selectionColor | ⚡ |
Supports focus:, active:, disabled: state selectors.
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
columnWrapperClassName | columnWrapperStyle | — |
ListHeaderComponentClassName |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName |
| Prop | Maps to | Prefix |
|---|---|---|
thumbColorClassName | thumbColor | ⚡ accent- |
trackColorOnClassName | trackColor.true (on) | ⚡ accent- |
trackColorOffClassName | trackColor.false (off) |
Note: Switch does NOT support className (className?: never in types). Use only the color-specific className props above. Supports disabled: state selector.
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
colorClassName | color | ⚡ accent- |
| Prop | Maps to | Prefix |
|---|---|---|
colorClassName | color | ⚡ accent- |
Note: Button does not support className (no style prop on RN Button).
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
backdropColorClassName | backdropColor | ⚡ accent- |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
colorsClassName | colors (Android) | ⚡ accent- |
tintColorClassName | tintColor (iOS) | ⚡ accent- |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
imageClassName | imageStyle | — |
tintColorClassName | tintColor | ⚡ accent- |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
backgroundColorClassName | backgroundColor | ⚡ accent- |
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
underlayColorClassName | underlayColor | ⚡ accent- |
Supports active:, disabled: state selectors.
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
Supports active:, disabled: state selectors.
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
Supports active:, disabled: state selectors.
| Prop | Maps to | Prefix |
|---|---|---|
className | style | — |
Supports active:, disabled: state selectors.
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>
React Native components have props like color, tintColor, thumbColor that are NOT part of the style object. To set these via Tailwind classes, use the accent- prefix with the corresponding {propName}ClassName prop:
// 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" />
CRITICAL Rule : className maps to the style prop — it handles layout, typography, backgrounds, borders, etc. But React Native has many color props that live OUTSIDE of style (like color, tintColor, thumbColor, placeholderTextColor). These require a separate {propName}ClassName prop with the accent- prefix. Without accent-, the class resolves to a style object — but these props expect a plain color string.
// 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} />
Wrap once at module level, use with className everywhere:
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 automatically maps:
style → className{name}Style → {name}ClassName{name}Color → {name}ColorClassName (with accent- prefix)For custom prop mappings:
const StyledProgressBar = withUniwind(ProgressBar, {
width: {
fromClassName: 'widthClassName',
styleProperty: 'width',
},
});
Usage patterns:
Used in one file only — define the wrapped component in that same file
Used across multiple files — wrap once in a shared module (e.g., components/styled.ts) and re-export
// 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';
NEVER call withUniwind on the same component in multiple files.
CRITICAL : Do NOT use withUniwind on components from react-native or react-native-reanimated. These already have built-in className support:
// 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
Converts Tailwind class strings to React Native style objects. Use for one-off cases or components that only accept 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 }} />
| Feature | withUniwind | useResolveClassNames |
|---|---|---|
| Setup | Once per component | Per usage |
| Performance | Optimized | Slightly slower |
| Best for | Reusable components | One-off, navigation config |
| Syntax | className="..." | style={...} |
// 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(' ')} />
For complex component styling with variants and compound variants:
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>
Uniwind does NOT auto-deduplicate conflicting classNames. This means if the same property appears in multiple classes, both will be applied and the result is unpredictable. This is especially critical when mixing custom CSS classes with Tailwind utilities.
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-primary', disabled && 'opacity-50')} />
2. CRITICAL: Mixing custom CSS classes with Tailwind utilities — if your custom CSS class sets a property that a Tailwind utility also sets, you MUST use cn() to deduplicate:
/* 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')} />
3. tv() output combined with extra classes — tv already handles its own variants, but if you add more classes on top:
<Pressable className={cn(button({ color: 'primary' }), props.className)} />
<View className="flex-1 p-4 bg-white" /><View className="card-shadow mt-4" /> (if card-shadow only sets box-shadow which no Tailwind class also sets)Works immediately — no configuration needed:
<View className="bg-white dark:bg-gray-900">
<Text className="text-black dark:text-white">Themed</Text>
</View>
Best for small apps and prototyping. Does not scale to custom themes.
Define in global.css, use everywhere without dark: prefix:
@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>
Variable naming: --color-background → bg-background, text-background.
Prefer CSS variables over explicitdark: variants — they're cleaner, maintain easier, and work with custom themes automatically.
Step 1 — Define in 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 */
}
}
}
Step 2 — Register in metro.config.js (exclude light/dark — they're automatic):
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
extraThemes: ['ocean'],
});
Restart Metro after adding themes.
Step 3 — Use:
Uniwind.setTheme('ocean');
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') also calls Appearance.setColorScheme to sync native components (Alert, Modal, system dialogs).
By default Uniwind uses "system" theme - follows device color scheme. If user wants to override it, just call Uniwind.setTheme with desired theme. It can be done above the React component to avoid theme switching at runtime.
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>
);
};
Apply a different theme to a subtree without changing the global theme:
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 wins (nested scopes supported)useUniwind, useResolveClassNames, useCSSVariable) resolve against the nearest scoped themewithUniwind-wrapped components inside the scope also resolve scoped theme valuesextraThemesAccess CSS variable values in JavaScript:
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]
Use for: animations, chart libraries, third-party component configs, calculations with design tokens.
It's required to cast the result of useCSSVariable as it can return: string | number | undefined. Uniwind doesn't know if given variable exist and what type it is, so it returns union type.
Update theme variables at runtime (e.g., user-selected brand colors or API-driven themes):
Uniwind.updateCSSVariables('light', {
'--color-primary': '#ff6600',
'--color-background': '#fafafa',
});
Updates are theme-specific and take effect immediately.
For JS-only values not used in classNames:
@theme static {
--chart-line-width: 2;
--chart-dot-radius: 4;
--animation-duration: 300;
}
Access via useCSSVariable('--chart-line-width'). Use for: chart configs, animation durations, native module values.
Perceptually uniform color format — wider gamut, consistent lightness:
@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);
}
}
}
Apply platform-specific styles directly in 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" />
Platform variants in @layer theme for global values (use @variant, not @media):
@layer theme {
:root {
@variant ios { --font-sans: 'SF Pro Text'; }
@variant android { --font-sans: 'Roboto-Regular'; }
@variant web { --font-sans: 'Inter'; }
}
}
Prefer platform selectors overPlatform.select() — cleaner syntax, no imports needed.
Style based on prop values using data-[prop=value]:utility:
// 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>
Rules :
data-[prop=value])data-[prop] — not supported)has-data-* parent selectors (not supported in 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>
Components with state support:
active:, disabled:, focus:active:, disabled:, focus:disabled:active:, disabled:active:, Mobile-first — unprefixed styles apply to all sizes, prefixed styles apply at that breakpoint and above:
| Prefix | Min Width | Typical Device |
|---|---|---|
| (none) | 0px | All (mobile) |
sm: | 640px | Large phones |
md: | 768px | Tablets |
lg: | 1024px | Landscape tablets |
xl: | 1280px | Desktops |
2xl: |
// 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>
Custom breakpoints:
@theme {
--breakpoint-xs: 480px;
--breakpoint-tablet: 820px;
--breakpoint-3xl: 1920px;
}
Usage: xs:p-2 tablet:p-4 3xl:p-8
Design mobile-first — start with base styles (no prefix), enhance with breakpoints:
// 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" />
| Class | Description |
|---|---|
p-safe | All sides |
pt-safe / pb-safe / pl-safe / pr-safe | Individual sides |
px-safe / py-safe | Horizontal / vertical |
| Class | Description |
|---|---|
m-safe | All sides |
mt-safe / mb-safe / ml-safe / mr-safe | Individual sides |
mx-safe / my-safe | Horizontal / vertical |
| Class | Description |
|---|---|
inset-safe | All sides |
top-safe / bottom-safe / left-safe / right-safe | Individual sides |
x-safe / y-safe | Horizontal / vertical inset |
| Pattern | Behavior | Example |
|---|---|---|
{prop}-safe-or-{value} | Math.max(inset, value) — ensures minimum spacing | pt-safe-or-4 |
{prop}-safe-offset-{value} | inset + value — adds extra spacing on top of inset | pb-safe-offset-4 |
Uniwind Free (default) — requires react-native-safe-area-context to update insets. Wrap your App component in SafeAreaProvider and SafeAreaListener and call Uniwind.updateInsets(insets) in the onChange callback:
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 — automatic, no setup needed. Insets injected from native layer.
Uniwind provides CSS functions for device-aware and theme-aware styling. 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 utility classes:
Returns the thinnest line width displayable on the device. Use for subtle borders and dividers.
@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" />
Multiplies a base value by the device's font scale accessibility setting. Ensures text respects user preferences for larger or smaller text.
fontScale() — uses multiplier 1 (device font scale × 1)
fontScale(0.9) — smaller scale
fontScale(1.2) — larger scale
@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>
Multiplies a value by the device's pixel ratio. Creates pixel-perfect designs that scale across screen densities.
pixelRatio() — uses multiplier 1 (device pixel ratio × 1)
pixelRatio(2) — double the pixel ratio
@utility w-icon { width: pixelRatio(); } @utility w-avatar { width: pixelRatio(2); }
<Image source={{ uri: 'avatar.png' }} className="w-avatar rounded-full" />
Returns different values based on the current theme mode. Automatically adapts when the theme changes — no manual switching logic needed.
First parameter: value for light theme
Second parameter: value for dark theme
@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>Also works in custom CSS classes (not just @utility):
.adaptive-card {
background-color: light-dark(#ffffff, #1f2937);
color: light-dark(#111827, #f9fafb);
}
Uniwind supports custom CSS class names defined in global.css. They are compiled at build time — no runtime overhead. Use them when you need styles that are hard to express as Tailwind utilities (e.g., complex box-shadow, multi-property bundles).
/* 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;
}
Apply via className just like any Tailwind class:
<View className="card-shadow" />
You can combine custom CSS classes with Tailwind utilities in a single className:
<View className="card-shadow p-4 m-2">
<Text className="adaptive-surface mb-2">{title}</Text>
<View className="container flex-row">{children}</View>
</View>
WARNING : If a custom CSS class and a Tailwind utility set the same property , you MUST use cn() to deduplicate. Without cn(), both values apply and the result is unpredictable:
// 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')} />
Rule of thumb : If your custom CSS class sets properties that might overlap with Tailwind utilities you'll also use, always wrap with cn(). See cn Utility section for full setup.
light-dark() for theme-aware custom classes.card, .chip, .badge-dot)The @utility directive creates utility classes that work exactly like built-in Tailwind classes. Three main use cases:
Create a utility whose value comes from a CSS variable injected at runtime via updateCSSVariables. Use @theme static to declare the variable so Uniwind tracks it even before it is updated:
/* global.css */
@theme static {
--header-height: 0px;
}
@utility p-safe-header {
padding-top: var(--header-height);
}
Inject the real value at runtime (e.g., from react-navigation's layout event):
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" />
For styles that have no built-in Tailwind class:
@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);
}
Usage like any Tailwind class: <View className="h-hairline card-shadow" />
Use @utility to completely replace what a built-in class does. Example: make border always use --color-primary:
@utility border {
border-width: 1px;
border-style: solid;
border-color: var(--color-primary);
}
Customize Tailwind design tokens in global.css:
@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;
}
Usage: bg-brand-500, text-brand-900, font-sans, font-mono, rounded-lg.
React Native requires a single font per family — no fallbacks:
@theme {
--font-sans: 'Roboto-Regular';
--font-sans-bold: 'Roboto-Bold';
--font-mono: 'FiraCode-Regular';
}
Font name must exactly match the font file name (without extension).
Expo : Configure fonts in app.json with the expo-font plugin, then reference in CSS.
Bare RN : Use react-native-asset to link fonts, same CSS config.
Platform-specific fonts (use @variant, not @media):
@layer theme {
:root {
@variant ios { --font-sans: 'SF Pro Text'; }
@variant android { --font-sans: 'Roboto-Regular'; }
@variant web { --font-sans: 'system-ui'; }
}
}
Built-in support — no extra dependencies:
<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>
For expo-linear-gradient, you can wrap it with withUniwind for className-based layout and styling (padding, border-radius, flex, etc.), but the colors prop is an array that cannot be resolved via className — it must be provided explicitly. Use useCSSVariable to get theme-aware colors:
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>
);
}
Alternatively, export a wrapped component from a shared module for reuse:
// 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>
Use useResolveClassNames for screen options that only accept style objects:
import { useResolveClassNames } from 'uniwind';
function Layout() {
const headerStyle = useResolveClassNames('bg-background');
const headerTitleStyle = useResolveClassNames('text-foreground font-bold');
return (
<Stack.Navigator
screenOptions={{
headerStyle,
headerTitleStyle,
}}
/>
);
}
Keep React Navigation's <ThemeProvider> if already in use — it manages navigation-specific theming.
HeroUI Native : Works with Uniwind. Uses tailwind-variants (tv) internally. Apply className directly on HeroUI components. Bun users : Bun uses symlinks for node_modules, which can cause Tailwind's Oxide scanner to miss library classes in production builds. Fix: use the resolved path in @source and hoist the package:
@source "../../node_modules/heroui-native/lib";
# .npmrc
public-hoist-pattern[]=heroui-native
react-native-reusables : Compatible.
Gluestack v4.1+ : Compatible.
Lucide React Native : Use withUniwind(LucideIcon) with colorClassName="accent-blue-500" for icon color. Works for all Lucide icons.
Use semantic color tokens (bg-primary, text-foreground) for theme consistency across UI kits.
React Native uses the Yoga layout engine. Key differences from web CSS:
flexDirection: 'column'Layout, spacing, sizing, typography, colors, borders, effects, flexbox, positioning, transforms, interactive states.
hover: visited: — use Pressable active: insteadbefore: after: placeholder: — pseudo-elementsfloat-* clear-* columns-*print: screen:Paid upgrade with 100% API compatibility. Built on a 2nd-generation C++ engine for apps that demand the best performance. $99/seat (billed annually). Pricing and licensing: https://uniwind.dev/pricing
animate-* and transition-* via className (Reanimated v4)SafeAreaListener setup neededPackage: "uniwind": "npm:uniwind-pro@rc" in package.json.
Set dependency alias in package.json:
{ "dependencies": { "uniwind": "npm:uniwind-pro@rc" } }
Install peer dependencies:
npm install react-native-nitro-modules react-native-reanimated react-native-worklets
Authenticate: npx uniwind-pro (interactive — select "Login with GitHub")
Add Babel plugin:
// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['react-native-worklets/plugin'],
};
Whitelist postinstall if needed:
"trustedDependencies": ["uniwind"] to package.jsonPro does NOT work with Expo Go. Requires native rebuild.
Verify installation : Check for native modules (.cpp, .mm files) in node_modules/uniwind.
<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" />
Components auto-swap to Animated versions when animation classes detected:
| Component | Animated Version |
|---|---|
View | Animated.View |
Text | Animated.Text (iOS only — no Android support from Reanimated) |
Image | Animated.Image |
ImageBackground | Animated.ImageBackground |
Smooth property changes when className or state changes:
// 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 | No transition |
transition-all | All properties |
transition-colors | Background, border, text colors |
transition-opacity | Opacity |
transition-shadow | Box shadow |
transition-transform | Scale, rotate, translate |
Duration: duration-75 duration-100 duration-150 duration-200 duration-300 duration-500 duration-700 duration-1000
Easing: ease-linear ease-in ease-out ease-in-out
Delay: delay-75 delay-100 delay-150 delay-200 delay-300 delay-500 delay-700 delay-1000
Still works with Uniwind classes:
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>
)}
/>
No code changes needed — props connect directly to C++ engine, eliminating re-renders automatically.
Remove SafeAreaListener setup — insets injected from native layer:
// With Pro — just use safe area classes directly
<View className="pt-safe pb-safe">{/* content */}</View>
Native animated transitions when switching themes. Import ThemeTransitionPreset and pass to setTheme:
import { Uniwind, ThemeTransitionPreset } from 'uniwind';
// Fade transition
Uniwind.setTheme('dark', ThemeTransitionPreset.Fade);
// Slide transitions
Uniwind.setTheme('dark', ThemeTransitionPreset.SlideRightToLeft);
Uniwind.setTheme('light', ThemeTransitionPreset.SlideLeftToRight);
// Circle mask transitions (expand from a corner or center)
Uniwind.setTheme('ocean', ThemeTransitionPreset.CircleCenter);
Uniwind.setTheme('dark', ThemeTransitionPreset.CircleTopRight);
// No animation
Uniwind.setTheme('light', ThemeTransitionPreset.None);
Available presets:
| Preset | Effect |
|---|---|
ThemeTransitionPreset.None | Instant switch, no animation |
ThemeTransitionPreset.Fade | Crossfade between themes |
ThemeTransitionPreset.SlideRightToLeft | Slide new theme in from right |
ThemeTransitionPreset.SlideLeftToRight | Slide new theme in from left |
ThemeTransitionPreset.CircleTopRight | Circle mask expanding from top-right |
ThemeTransitionPreset.CircleTopLeft |
When styles aren't working, check in this order:
"uniwind" (or "uniwind-pro") in dependencies"tailwindcss" at v4+ (^4.0.0)react-native-nitro-modules, react-native-reanimated, react-native-workletswithUniwindConfig imported from 'uniwind/metro'withUniwindConfig is the outermost wrappercssEntryFile is a relative path string (e.g., './global.css')path.resolve() or absolute paths@import 'tailwindcss'; AND @import 'uniwind';App.tsx or root layout, NOT in index.ts/index.js'react-native-worklets/plugin' in plugins arrayuniwind-types.d.ts exists (generated after running Metro)tsconfig.json or placed in src//app/ dirnpx expo start --clear or npx react-native start --reset-cache)| Symptom | Cause | Fix |
|---|---|---|
| Styles not applying | Missing imports in global.css | Add @import 'tailwindcss'; @import 'uniwind'; |
| Styles not applying | global.css imported in index.js | Move import to App.tsx or _layout.tsx |
| Classes not detected | global.css in nested dir, components elsewhere | Add @source '../components' in global.css |
| TypeScript errors on className | Missing types file | Run Metro to generate uniwind-types.d.ts |
withUniwindConfig is not a function |
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
103,800 周安装
OpenAPI 转 TypeScript 工具 - 自动生成 API 接口与类型守卫
563 周安装
数据库模式设计器 - 内置最佳实践,自动生成生产级SQL/NoSQL数据库架构
564 周安装
Rust Unsafe代码检查器 - 安全使用Unsafe Rust的完整指南与最佳实践
564 周安装
.NET并发编程模式指南:async/await、Channels、Akka.NET选择决策树
565 周安装
韩语语法检查器 - 基于国立国语院标准的拼写、空格、语法、标点错误检测与纠正
565 周安装
技能安全扫描器 - 检测Claude技能安全漏洞,防范提示注入与恶意代码
565 周安装
Animated.ViewAnimated.TextclassNamewithUniwindwithUniwindexpo-imageexpo-blurmotitext-blue-500classNamestyleaccent-accent-placeholderTextColorClassName | placeholderTextColor | ⚡ accent- |
selectionHandleColorClassName | selectionHandleColor | ⚡ accent- |
underlineColorAndroidClassName | underlineColorAndroid (Android) | ⚡ accent- |
ListHeaderComponentStyle |
| — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
ListFooterComponentStyle |
| — |
endFillColorClassName | endFillColor | ⚡ accent- |
ListFooterComponentStyle |
| — |
endFillColorClassName | endFillColor | ⚡ accent- |
⚡ accent- |
ios_backgroundColorClassName | ios_backgroundColor (iOS) | ⚡ accent- |
titleColorClassName | titleColor (iOS) | ⚡ accent- |
progressBackgroundColorClassName | progressBackgroundColor (Android) | ⚡ accent- |
disabled:| 1536px |
| Large desktops |
@shopify/flash-list : Use withUniwind(FlashList) for className and contentContainerClassName support. Note: withUniwind loses generic type params on ref — cast manually if needed.
break-before-* break-after-* break-inside-*.yarnrc.ymlpnpm config set enable-pre-post-scripts trueRebuild native app:
npx expo prebuild --clean && npx expo run:ios
ScrollView | Animated.ScrollView |
FlatList | Animated.FlatList |
TextInput | Animated.TextInput (iOS only) |
Pressable | Animated.View wrapper |
| Circle mask expanding from top-left |
ThemeTransitionPreset.CircleBottomRight | Circle mask expanding from bottom-right |
ThemeTransitionPreset.CircleBottomLeft | Circle mask expanding from bottom-left |
ThemeTransitionPreset.CircleCenter | Circle mask expanding from center |
| Wrong import |
Use require('uniwind/metro') not require('uniwind') |
| Hot reload full-reloads | global.css imported in wrong file | Move to App.tsx or root layout |
cssEntryFile error / Metro crash | Absolute path used | Use relative: './global.css' |
withUniwindConfig not outermost | Another wrapper wraps Uniwind | Swap order so Uniwind is outermost |
| Dark theme not working | Missing @variant dark | Define dark variant in @layer theme |
| Custom theme not appearing | Not registered in metro config | Add to extraThemes array, restart Metro |
| Fonts not loading | Font name mismatch | CSS font name must match file name exactly (no extension) |
rem values too large/small | Wrong base rem | Set polyfills: { rem: 14 } for NativeWind compat |
| Unsupported CSS warning | Web-specific CSS used | Enable debug: true to identify; remove unsupported properties |
Failed to serialize javascript object | Complex CSS, circular refs, or stale cache | Clear caches: watchman watch-del-all; rm -rf node_modules/.cache; npx expo start --clear. Also check if docs/markdown files containing CSS classes are in the scan path (see below) |
Failed to serialize javascript object from llms-full.txt or docs | Docs/markdown files with CSS classes in project dir get scanned by Tailwind | Move .md files with CSS examples outside the project root, or add to .gitignore so Tailwind's scanner skips them |
unstable_enablePackageExports conflict | App disables package exports | Use selective resolver for Uniwind and culori |
| Classes from monorepo package missing | Not included in Tailwind scan | Add @source '../../packages/ui' in global.css |
Classes from node_modules library missing in production (bun) | Bun uses symlinks; Tailwind's Oxide scanner can't follow them | Use resolved path: @source "../../node_modules/heroui-native/lib" and add public-hoist-pattern[]=heroui-native to .npmrc |
active: not working with withUniwind | withUniwind does NOT support interactive state selectors | Only core RN Pressable/TextInput/Switch support active:/focus:/disabled:. Third-party pressables wrapped with withUniwind won't get states |
withUniwind custom mapping overrides className+style merging | When manual mapping is provided, style prop is not merged | Use auto mapping (no second arg) for className+style merge. For manual mapping + className, double-wrap: withUniwind(withUniwind(Comp), { mapping }) |
withUniwind loses generic types on ref (e.g., FlashList<T>) | TypeScript limitation with HOCs | Cast the ref manually: ref={scrollRef as any} |
Platform-specific fonts: @theme block error | @media ios/android inside @theme {} | Use @layer theme { :root { @variant ios { ... } } } instead — @theme only accepts custom properties, and platform selection uses @variant not @media |
Uniwind.setTheme('system') crash on Android (RN 0.82+) | RN 0.82 changed Appearance API | Update to latest Uniwind (fixed). Avoid setTheme('system') on older Uniwind + RN 0.82+ |
| Styles flash/disappear on initial load (Android) | SafeAreaListener fires before component listeners mount | Fixed in recent versions. If persists, ensure Uniwind is latest |
useTVEventHandler is undefined | Uniwind module replacement interferes with tvOS exports | Fixed in v1.2.1+. Update Uniwind |
@layer theme variables not rendering on web | Bug with RNW + Expo SDK 55 | Fixed in v1.4.1+. Update Uniwind |
updateCSSVariables wrong theme at app start | Calling for multiple themes back-to-back; last call wins on first render | Call updateCSSVariables for the current theme last. After initial load, order doesn't matter |
| Pro: animations not working | Missing Babel plugin | Add react-native-worklets/plugin to babel.config.js |
| Pro: module not found | No native rebuild | Run npx expo prebuild --clean then npx expo run:ios |
| Pro: postinstall failed | Package manager blocks scripts | Add to trustedDependencies (bun) or configure yarn |