Uniwind — Complete Reference
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.
Critical Rules
- Tailwind v4 only — Use
@import 'tailwindcss'not@tailwind base. Tailwind v3 is not supported. - Never construct classNames dynamically — Tailwind scans at build time.
bg-${color}-500will NOT work. Use complete string literals, mapping objects, or ternaries. - Never use
cssInteroporremapProps— Those are NativeWind APIs. Uniwind does not override global components. - No
tailwind.config.js— All config goes inglobal.cssvia@themeand@layer theme. - No ThemeProvider required — Use
Uniwind.setTheme()directly. withUniwindConfigmust be the outermost Metro config wrapper.- NEVER wrap
react-nativeorreact-native-reanimatedcomponents withwithUniwind—View,Text,Pressable,Image,TextInput,ScrollView,FlatList,Switch,Modal,Animated.View,Animated.Text, etc. already have fullclassNamesupport built in. Wrapping them withwithUniwindwill break behavior. Only usewithUniwindfor third-party components (e.g.,expo-image,expo-blur,moti). - Font families: single font only — React Native doesn't support fallbacks. Use
--font-sans: 'Roboto-Regular'not'Roboto', sans-serif. - All theme variants must define the same set of CSS variables — If
lightdefines--color-primary, thendarkand every custom theme must too. Mismatched variables cause runtime errors. accent-prefix is REQUIRED for non-style color props — This is crucial. Props likecolor(Button, ActivityIndicator),tintColor(Image),thumbColor(Switch),placeholderTextColor(TextInput) are NOT part of thestyleobject. You MUST use the corresponding{propName}ClassNameprop withaccent-prefixed classes. Example:<ActivityIndicator colorClassName="accent-blue-500" />NOT<ActivityIndicator className="text-blue-500" />. Regular Tailwind color classes (liketext-blue-500) only work onclassName(which maps tostyle). For non-style color props, always useaccent-.- rem default is 16px — NativeWind used 14px. Set
polyfills: { rem: 14 }in metro config if migrating. cssEntryFilemust be a relative path string — Use'./global.css'notpath.resolve(__dirname, 'global.css').- Deduplicate with
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 withcn('card', 'p-6')when there's overlap.
Setup
Installation
# or other package manager
bun install uniwind tailwindcss
Requires Tailwind CSS v4+.
global.css
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.
Metro Configuration
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);
Vite Configuration (v1.2.0+)
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',
}),
],
});
TypeScript
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.
Expo Router Placement
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';
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 Support
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).
Component Bindings
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).
Complete Reference
Legend: Props marked with ⚡ require the accent- prefix. Props in parentheses are platform-specific.
View
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
Text
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
selectionColorClassName |
selectionColor |
⚡ accent- |
Pressable
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
Supports active:, disabled:, focus: state selectors.
Image
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
tintColorClassName |
tintColor |
⚡ accent- |
TextInput
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
cursorColorClassName |
cursorColor |
⚡ accent- |
selectionColorClassName |
selectionColor |
⚡ accent- |
placeholderTextColorClassName |
placeholderTextColor |
⚡ accent- |
selectionHandleColorClassName |
selectionHandleColor |
⚡ accent- |
underlineColorAndroidClassName |
underlineColorAndroid (Android) |
⚡ accent- |
Supports focus:, active:, disabled: state selectors.
ScrollView
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
contentContainerClassName |
contentContainerStyle |
— |
endFillColorClassName |
endFillColor |
⚡ accent- |
FlatList
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
contentContainerClassName |
contentContainerStyle |
— |
columnWrapperClassName |
columnWrapperStyle |
— |
ListHeaderComponentClassName |
ListHeaderComponentStyle |
— |
ListFooterComponentClassName |
ListFooterComponentStyle |
— |
endFillColorClassName |
endFillColor |
⚡ accent- |
SectionList
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
contentContainerClassName |
contentContainerStyle |
— |
ListHeaderComponentClassName |
ListHeaderComponentStyle |
— |
ListFooterComponentClassName |
ListFooterComponentStyle |
— |
endFillColorClassName |
endFillColor |
⚡ accent- |
VirtualizedList
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
contentContainerClassName |
contentContainerStyle |
— |
ListHeaderComponentClassName |
ListHeaderComponentStyle |
— |
ListFooterComponentClassName |
ListFooterComponentStyle |
— |
endFillColorClassName |
endFillColor |
⚡ accent- |
Switch
| Prop | Maps to | Prefix |
|---|---|---|
thumbColorClassName |
thumbColor |
⚡ accent- |
trackColorOnClassName |
trackColor.true (on) |
⚡ accent- |
trackColorOffClassName |
trackColor.false (off) |
⚡ accent- |
ios_backgroundColorClassName |
ios_backgroundColor (iOS) |
⚡ accent- |
Note: Switch does NOT support className (className?: never in types). Use only the color-specific className props above. Supports disabled: state selector.
ActivityIndicator
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
colorClassName |
color |
⚡ accent- |
Button
| Prop | Maps to | Prefix |
|---|---|---|
colorClassName |
color |
⚡ accent- |
Note: Button does not support className (no style prop on RN Button).
Modal
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
backdropColorClassName |
backdropColor |
⚡ accent- |
RefreshControl
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
colorsClassName |
colors (Android) |
⚡ accent- |
tintColorClassName |
tintColor (iOS) |
⚡ accent- |
titleColorClassName |
titleColor (iOS) |
⚡ accent- |
progressBackgroundColorClassName |
progressBackgroundColor (Android) |
⚡ accent- |
ImageBackground
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
imageClassName |
imageStyle |
— |
tintColorClassName |
tintColor |
⚡ accent- |
SafeAreaView
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
KeyboardAvoidingView
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
contentContainerClassName |
contentContainerStyle |
— |
InputAccessoryView
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
backgroundColorClassName |
backgroundColor |
⚡ accent- |
TouchableHighlight
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
underlayColorClassName |
underlayColor |
⚡ accent- |
Supports active:, disabled: state selectors.
TouchableOpacity
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
Supports active:, disabled: state selectors.
TouchableNativeFeedback
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
Supports active:, disabled: state selectors.
TouchableWithoutFeedback
| Prop | Maps to | Prefix |
|---|---|---|
className |
style |
— |
Supports active:, disabled: state selectors.
Usage Examples
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" />
// 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" />
</InputAccessoryView>
// TouchableHighlight — underlay color
<TouchableHighlight
className="bg-blue-500 px-6 py-3 rounded-lg"
underlayColorClassName="accent-blue-600 dark:accent-blue-700"
>
<Text className="text-white font-semibold">Press Me</Text>
</TouchableHighlight>
The accent- Prefix Pattern
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} />
Styling Third-Party Components
withUniwind (Recommended)
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
useResolveClassNames
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 }} />
Comparison
| Feature | withUniwind | useResolveClassNames |
|---|---|---|
| Setup | Once per component | Per usage |
| Performance | Optimized | Slightly slower |
| Best for | Reusable components | One-off, navigation config |
| Syntax | className="..." |
style={...} |
Dynamic ClassNames
NEVER do this (Tailwind scans at build time)
// BROKEN — template literal with variable
<View className={`bg-${color}-500`} />
<Text className={`text-${size}`} />
Correct patterns
// 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)
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>
cn Utility — Class Deduplication
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.
Setup
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));
}
When cn Is Required
- Merging className props — component accepts external className that may conflict:
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')} />
- 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')} />
- 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)} />
When cn Is NOT Needed
- Static className with no conflicts:
<View className="flex-1 p-4 bg-white" /> - Single custom CSS class with no overlapping Tailwind:
<View className="card-shadow mt-4" />(if card-shadow only sets box-shadow which no Tailwind class also sets)
Theming
Quick Setup (dark: prefix)
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.
Scalable Setup (CSS Variables)
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 explicit dark: variants — they're cleaner, maintain easier, and work with custom themes automatically.
Custom Themes
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');
Theme 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') 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.
Theme Switcher Example
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}
=> 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
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>
- Nearest
ScopedThemewins (nested scopes supported) - Hooks (
useUniwind,useResolveClassNames,useCSSVariable) resolve against the nearest scoped theme withUniwind-wrapped components inside the scope also resolve scoped theme values- Custom themes require registration in
extraThemes
useCSSVariable
Access 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.
Runtime CSS Variable Updates
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.
@theme static
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.
OKLCH Colors support
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);
}
}
}
Platform Selectors
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 over Platform.select() — cleaner syntax, no imports needed.
Data Selectors
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:
- Only equality selectors supported (
data-[prop=value]) - No presence-only selectors (
data-[prop]— not supported) - No
has-data-*parent selectors (not supported in React Native) - Booleans match both boolean and string forms
Interactive States
// 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:
- Pressable:
active:,disabled:,focus: - TextInput:
active:,disabled:,focus: - Switch:
disabled: - Text:
active:,disabled: - TouchableOpacity / TouchableHighlight / TouchableNativeFeedback / TouchableWithoutFeedback:
active:,disabled:
Responsive Breakpoints
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: |
1536px | Large desktops |
// 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" />
Safe Area Utilities
Padding
| Class | Description |
|---|---|
p-safe |
All sides |
pt-safe / pb-safe / pl-safe / pr-safe |
Individual sides |
px-safe / py-safe |
Horizontal / vertical |
Margin
| Class | Description |
|---|---|
m-safe |
All sides |
mt-safe / mb-safe / ml-safe / mr-safe |
Individual sides |
mx-safe / my-safe |
Horizontal / vertical |
Positioning
| Class | Description |
|---|---|
inset-safe |
All sides |
top-safe / bottom-safe / left-safe / right-safe |
Individual sides |
x-safe / y-safe |
Horizontal / vertical inset |
Compound Variants
| 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 |
Setup
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
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.
CSS Functions
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:
hairlineWidth()
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" />
fontScale(multiplier?)
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 scalefontScale(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>
pixelRatio(multiplier?)
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" />
light-dark(lightValue, darkValue)
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);
}
Custom CSS & Utilities
Custom CSS Classes
Uniwind supports custom CSS class names defined
…(truncated)