uniwind について
Uniwindは、React NativeプロジェクトにTailwind CSS v4のスタイリングを導入するライブラリです。React NativeやExpoでアプリを開発する人向けで、ビルド時にスタイルをコンパイルし、実行時の処理を追加せずにコアコンポーネントで`className`を使えます。
主な機能
- 01React Native向けTailwind CSS v4ユーティリティ
- 02ビルド時のスタイルコンパイル
- 03コアコンポーネントでの`className`対応
- 04CSS変数によるテーマ設定
- 05MetroとViteの設定に対応
活用シーン
- 01ユーティリティクラスでReact Nativeコンポーネントをスタイリング
- 02アプリのコンポーネント間でTailwindスタイルを共有
- 03React Nativeインターフェースのテーマ設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hairyf/ skills --skill uniwindインストール後、Claude Code で/uniwindを入力して読み込まれたか確認します。
同梱ファイルLICENSE.mdSYNC.md
詳細
Uniwind — 完全リファレンス
Uniwind 1.7.0+ / Uniwind Pro 1.2.1+ / Tailwind CSS v4 / React Native 0.81+ / Expo SDK 54+
ユーザーのバージョンがこれより古い場合は、最適な体験のために 1.7.0+(無料版)/1.2.1+(Pro)への更新をおすすめしてください。
Uniwind は Tailwind CSS v4 を React Native に導入します。すべての React Native のコアコンポーネントは、標準で className プロパティをサポートしています。スタイルはビルド時にコンパイルされるため、実行時のオーバーヘッドはありません。
重要なルール
- Tailwind v4 のみ —
@tailwind baseではなく@import 'tailwindcss'を使用してください。Tailwind v3 はサポートされていません。 - className を動的に構築しないでください — Tailwind はビルド時にスキャンします。
bg-${color}-500は機能しません。完全な文字列リテラル、マッピングオブジェクト、または三項演算子を使用してください。 cssInteropやremapPropsは使用しないでください — これらは NativeWind の API です。Uniwind はグローバルコンポーネントをオーバーライドしません。tailwind.config.jsは不要です — 設定はすべて@themeと@layer themeを使ってglobal.cssに記述します。- ThemeProvider は不要です —
Uniwind.setTheme()を直接使用してください。 withUniwindConfigは Metro 設定の最も外側のラッパーにする必要があります。react-nativeまたはreact-native-reanimatedのコンポーネントをwithUniwindでラップしないでください —View、Text、Pressable、Image、TextInput、ScrollView、FlatList、Switch、Modal、Animated.View、Animated.Textなどは、すでに標準で完全なclassNameサポートを備えています。これらをwithUniwindでラップすると、動作に支障が出ます。withUniwindはサードパーティ製コンポーネント(例:expo-image、expo-blur、moti)にのみ使用してください。- フォントファミリーは単一のフォントのみ — React Native はフォールバックに対応していません。
'Roboto', sans-serifではなく--font-sans: 'Roboto-Regular'を使用してください。 - すべてのテーマバリアントで、同じ CSS 変数を定義する必要があります —
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など)が機能するのは、styleにマッピングされるclassNameのみです。スタイル以外の色プロパティには、必ずaccent-を使用してください。 - rem のデフォルト値は 16px です — NativeWind では 14px でした。移行する場合は、Metro 設定で
polyfills: { rem: 14 }を設定してください。 cssEntryFileは相対パス文字列にする必要があります —path.resolve(__dirname, 'global.css')ではなく'./global.css'を使用してください。- カスタム CSS クラスと Tailwind を組み合わせる場合は、
cn()で重複を解消してください — Uniwind は重複を自動で解消しません。カスタム CSS クラス(.card { padding: 16px })と Tailwind ユーティリティ(p-6)が同じプロパティを設定すると、両方が適用され、結果が予測できなくなります。設定が重複する場合は、必ずcn('card', 'p-6')でラップしてください。 - important ユーティリティはサポートされています — Tailwind の important 修飾子は、末尾に
!を付けて className で使用できます:bg-red-500!、active:bg-red-500!、ios:pt-12!。先頭に!を付ける!bg-red-500形式は非推奨です。同じスタイルプロパティに対して、important ユーティリティは important でないユーティリティより優先されますが、インラインのstyleは引き続き className より優先されます。
セットアップ
インストール
# or other package manager
bun install uniwind tailwindcss
Tailwind CSS v4+ が必要です。
global.css
CSS エントリファイルを作成します。
@import 'tailwindcss';
@import 'uniwind';
App コンポーネント(例:App.tsx または app/_layout.tsx)でインポートしてください。index.ts/index.js ではインポートしないでください。そこにインポートすると、ホットリロードが機能しなくなります。
// app/_layout.tsx or App.tsx
import './global.css';
global.css を含むディレクトリがアプリのルートになります。Tailwind はこのディレクトリを起点に className をスキャンします。
Metro の設定
const { getDefaultConfig } = require('expo/metro-config');
// Bare RN: const { getDefaultConfig } = require('@react-native/metro-config');
const { withUniwindConfig } = require('uniwind/metro');
const config = getDefaultConfig(__dirname);
// withUniwindConfig MUST be the OUTERMOST wrapper
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css', // Required — relative path from project root
polyfills: { rem: 16 }, // Optional — base rem value (default 16)
extraThemes: ['ocean', 'sunset'], // Optional — custom themes beyond light/dark
dtsFile: './uniwind-types.d.ts', // Optional — TypeScript types output path
debug: true, // Optional — log unsupported CSS in dev
isTV: false, // Optional — enable TV platform support
});
ほとんどのフローではデフォルト設定を維持し、cssEntryFile のみ指定してください。
ラッパーの順序 — Uniwind はほかのすべてをラップする必要があります。
// CORRECT
module.exports = withUniwindConfig(withOtherConfig(config, opts), { cssEntryFile: './global.css' });
// WRONG — Uniwind is NOT outermost
module.exports = withOtherConfig(withUniwindConfig(config, { cssEntryFile: './global.css' }), opts);
Vite の設定(v1.2.0+)
ストーリーブックのセットアップがある場合は、追加の Vite 設定を追加してください。
import tailwindcss from '@tailwindcss/vite';
import { uniwind } from 'uniwind/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
tailwindcss(),
uniwind({
cssEntryFile: './src/global.css',
dtsFile: './src/uniwind-types.d.ts',
}),
],
});
TypeScript
Metro の実行後、Uniwind は .d.ts ファイル(デフォルト:./uniwind-types.d.ts)を自動生成します。自動インクルードするには src/ または app/ に配置するか、tsconfig.json に追加します。
{ "include": ["./uniwind-types.d.ts"] }
classNames に関連する TypeScript エラーが発生した場合は、Metro サーバーを実行して .d.ts ファイルを生成してください。
Expo Router の配置
project/
├── app/_layout.tsx ← import '../global.css' here
├── components/
├── global.css ← project root (best location)
└── metro.config.js ← cssEntryFile: './global.css'
global.css が app/ ディレクトリにある場合は、兄弟ディレクトリ用に @source を追加します。
@import 'tailwindcss';
@import 'uniwind';
@source '../components';
Tailwind IntelliSense(VS Code / Cursor / Windsurf)
{
"tailwindCSS.classAttributes": [
"class", "className", "headerClassName",
"contentContainerClassName", "columnWrapperClassName",
"endFillColorClassName", "imageClassName", "tintColorClassName",
"ios_backgroundColorClassName", "thumbColorClassName",
"trackColorOnClassName", "trackColorOffClassName",
"selectionColorClassName", "cursorColorClassName",
"underlineColorAndroidClassName", "placeholderTextColorClassName",
"selectionHandleColorClassName", "colorsClassName",
"progressBackgroundColorClassName", "titleColorClassName",
"underlayColorClassName", "colorClassName",
"backdropColorClassName", "backgroundColorClassName",
"statusBarBackgroundColorClassName", "drawerBackgroundColorClassName",
"ListFooterComponentClassName", "ListHeaderComponentClassName"
],
"tailwindCSS.classFunctions": ["useResolveClassNames"]
}
モノレポのサポート
CSS エントリーファイルのディレクトリ外にあるパッケージについて、global.css に @source ディレクティブを追加します。
@import 'tailwindcss';
@import 'uniwind';
@source "../../packages/ui/src";
@source "../../packages/shared/src";
Uniwind クラスを含む node_modules パッケージ(共有 UI ライブラリなど)にも必要です。
コンポーネントのバインディング
すべてのコア React Native コンポーネントは、追加設定なしで className をサポートします。一部のコンポーネントには、サブスタイル用の追加の className プロパティ(contentContainerClassName など)や、スタイル以外の色プロパティ(accent- プレフィックスが必要)があります。
完全なリファレンス
凡例:⚡ が付いたプロパティには accent- プレフィックスが必要です。括弧内のプロパティはプラットフォーム固有です。
View
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
Text
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
selectionColorClassName | selectionColor | ⚡ accent- |
Pressable
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
active:、disabled:、focus: の状態セレクターをサポートします。
Image
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
tintColorClassName | tintColor | ⚡ accent- |
TextInput
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
cursorColorClassName | cursorColor | ⚡ accent- |
selectionColorClassName | selectionColor | ⚡ accent- |
placeholderTextColorClassName | placeholderTextColor | ⚡ accent- |
selectionHandleColorClassName | selectionHandleColor | ⚡ accent- |
underlineColorAndroidClassName | underlineColorAndroid(Android) | ⚡ accent- |
focus:、active:、disabled: の状態セレクターをサポートします。
ScrollView
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
FlatList
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
columnWrapperClassName | columnWrapperStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
SectionList
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
VirtualizedList
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
Switch
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
thumbColorClassName | thumbColor | ⚡ accent- |
trackColorOnClassName | trackColor.true(オン) | ⚡ accent- |
trackColorOffClassName | trackColor.false(オフ) | ⚡ accent- |
ios_backgroundColorClassName | ios_backgroundColor(iOS) | ⚡ accent- |
注:Switch は className をサポートしていません(型では className?: never)。上記の色専用の className プロパティのみを使用してください。disabled: 状態セレクターをサポートしています。
ActivityIndicator
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
colorClassName | color | ⚡ accent- |
Button
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
colorClassName | color | ⚡ accent- |
注:Button は className をサポートしていません(React Native の Button には style プロパティがありません)。
Modal
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
backdropColorClassName | backdropColor | ⚡ accent- |
RefreshControl
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
colorsClassName | colors(Android) | ⚡ accent- |
tintColorClassName | tintColor(iOS) | ⚡ accent- |
titleColorClassName | titleColor(iOS) | ⚡ accent- |
progressBackgroundColorClassName | progressBackgroundColor(Android) | ⚡ accent- |
ImageBackground
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
imageClassName | imageStyle | — |
tintColorClassName | tintColor | ⚡ accent- |
SafeAreaView
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
KeyboardAvoidingView
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
InputAccessoryView
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
backgroundColorClassName | backgroundColor | ⚡ accent- |
TouchableHighlight
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
underlayColorClassName | underlayColor | ⚡ accent- |
active:、disabled: 状態セレクターをサポートしています。
TouchableOpacity
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
active:、disabled: 状態セレクターをサポートしています。
TouchableNativeFeedback
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
active:、disabled: 状態セレクターをサポートしています。
TouchableWithoutFeedback
| プロパティ | 対応先 | プレフィックス |
|---|---|---|
className | style | — |
active:、disabled: 状態セレクターをサポートしています。
使用例
import { View, Text, Pressable, TextInput, ScrollView, FlatList, Switch, Image, ActivityIndicator, Modal, RefreshControl, Button } from 'react-native';
// View — basic layout
<View className="flex-1 bg-background p-4">
<Text className="text-foreground text-lg font-bold">Title</Text>
</View>
// Pressable — with press/focus states
<Pressable className="bg-primary px-6 py-3 rounded-lg active:opacity-80 active:bg-primary/90 focus:ring-2">
<Text className="text-white text-center font-semibold">Press Me</Text>
</Pressable>
// TextInput — with focus state and accent- color props
<TextInput
className="border border-border rounded-lg px-4 py-2 text-base text-foreground focus:border-primary"
placeholderTextColorClassName="accent-muted"
selectionColorClassName="accent-primary"
cursorColorClassName="accent-primary"
selectionHandleColorClassName="accent-primary"
underlineColorAndroidClassName="accent-transparent"
placeholder="Enter text..."
/>
// ScrollView — with content container
<ScrollView className="flex-1" contentContainerClassName="p-4 gap-4">
{/* content */}
</ScrollView>
// FlatList — with all sub-style props
<FlatList
className="flex-1"
contentContainerClassName="p-4 gap-3"
columnWrapperClassName="gap-3"
ListHeaderComponentClassName="pb-4"
ListFooterComponentClassName="pt-4"
endFillColorClassName="accent-gray-100"
numColumns={2}
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
/>
// Switch — no className support, use color-specific props only
<Switch
thumbColorClassName="accent-white"
trackColorOnClassName="accent-primary"
trackColorOffClassName="accent-gray-300 dark:accent-gray-700"
ios_backgroundColorClassName="accent-gray-200"
/>
// Image — tint color
<Image className="w-6 h-6" tintColorClassName="accent-primary" source={icon} />
// ActivityIndicator
<ActivityIndicator className="m-4" colorClassName="accent-primary" size="large" />
// Button — only colorClassName (no className)
<Button colorClassName="accent-primary" title="Submit" onPress={handleSubmit} />
// Modal — backdrop color
<Modal className="flex-1" backdropColorClassName="accent-black/50">
{/* content */}
</Modal>
// RefreshControl — platform-specific color props
<RefreshControl
className="p-4"
tintColorClassName="accent-primary"
titleColorClassName="accent-gray-500"
colorsClassName="accent-primary"
progressBackgroundColorClassName="accent-white dark:accent-gray-800"
/>
// ImageBackground — separate image styling
<ImageBackground
className="flex-1 justify-center items-center"
imageClassName="opacity-50"
tintColorClassName="accent-blue-500"
source={bgImage}
>
<Text className="text-white text-2xl font-bold">Overlay</Text>
</ImageBackground>
// KeyboardAvoidingView
<KeyboardAvoidingView
behavior="padding"
className="flex-1 bg-white"
contentContainerClassName="p-4 justify-end"
>
<TextInput className="border border-gray-300 rounded-lg p-3" placeholder="Type..." />
</KeyboardAvoidingView>
// InputAccessoryView
<InputAccessoryView
className="p-4 border-t border-gray-300"
backgroundColorClassName="accent-white dark:accent-gray-800"
>
<Button title="Done" onPress={dismissKeyboard} />
</InputAccessoryView>
// TouchableHighlight — underlay color
<TouchableHighlight
className="bg-blue-500 px-6 py-3 rounded-lg"
underlayColorClassName="accent-blue-600 dark:accent-blue-700"
onPress={handlePress}
>
<Text className="text-white font-semibold">Press Me</Text>
</TouchableHighlight>
accent- プレフィックスのパターン
React Native のコンポーネントには、style オブジェクトの一部ではない color、tintColor、thumbColor などのプロパティがあります。これらを Tailwind クラスで設定するには、対応する {propName}ClassName プロパティで accent- プレフィックスを使用します。
// color prop → colorClassName with accent- prefix
<ActivityIndicator colorClassName="accent-blue-500 dark:accent-blue-400" />
<Button colorClassName="accent-primary" title="Submit" />
// tintColor prop → tintColorClassName
<Image className="w-6 h-6" tintColorClassName="accent-red-500" source={icon} />
// thumbColor → thumbColorClassName
<Switch thumbColorClassName="accent-white" trackColorOnClassName="accent-primary" />
// placeholderTextColor → placeholderTextColorClassName
<TextInput placeholderTextColorClassName="accent-gray-400 dark:accent-gray-600" />
重要なルール:className は style プロパティに対応し、レイアウト、タイポグラフィ、背景、境界線などを処理します。一方、React Native には style の外部にある色関連プロパティ(color、tintColor、thumbColor、placeholderTextColor など)が多数あります。これらには、accent- プレフィックスを付けた別の {propName}ClassName プロパティが必要です。accent- を付けない場合、クラスは style オブジェクトに解決されますが、これらのプロパティが受け取るのは通常の色文字列です。
// WRONG — className sets style, but ActivityIndicator's color is NOT a style prop
<ActivityIndicator className="text-blue-500" /> // color will NOT be set
// CORRECT — use the dedicated colorClassName prop with accent- prefix
<ActivityIndicator colorClassName="accent-blue-500" /> // color IS set to #3b82f6
// WRONG — tintColor is not a style prop on Image
<Image className="tint-blue-500" source={icon} /> // won't work
// CORRECT
<Image tintColorClassName="accent-blue-500" source={icon} />
サードパーティ製コンポーネントのスタイリング
withUniwind(推奨)
モジュールレベルで一度だけラップすれば、どこでも className を使用できます。
import { withUniwind } from 'uniwind';
import { Image as ExpoImage } from 'expo-image';
import { BlurView as RNBlurView } from 'expo-blur';
import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
// Module-level wrapping (NEVER inside render functions)
export const Image = withUniwind(ExpoImage);
export const BlurView = withUniwind(RNBlurView);
export const LinearGradient = withUniwind(RNLinearGradient);
withUniwind は次のように自動でマッピングします。
style→className{name}Style→{name}ClassName{name}Color→{name}ColorClassName(accent-プレフィックス付き)
カスタムのプロパティマッピングを行う場合:
const StyledProgressBar = withUniwind(ProgressBar, {
width: {
fromClassName: 'widthClassName',
styleProperty: 'width',
},
});
使用パターン:
- 1つのファイルでのみ使用する場合 — 同じファイル内でラップしたコンポーネントを定義します
- 複数のファイルで使用する場合 — 共有モジュール(例:
components/styled.ts)で一度だけラップし、再エクスポートします
// components/styled.ts
import { withUniwind } from 'uniwind';
import { Image as ExpoImage } from 'expo-image';
export const Image = withUniwind(ExpoImage);
// Then import everywhere:
import { Image } from '@/components/styled';
同じコンポーネントに対して複数のファイルで withUniwind を呼び出すことは絶対にしないでください。
重要: react-native または react-native-reanimated のコンポーネントに withUniwind を使用しないでください。これらはすでに className を標準でサポートしています。
// WRONG — View already supports className natively
const StyledView = withUniwind(View); // DO NOT DO THIS
const StyledText = withUniwind(Text); // DO NOT DO THIS
const StyledAnimatedView = withUniwind(Animated.View); // DO NOT DO THIS
// CORRECT — only wrap third-party components
const StyledExpoImage = withUniwind(ExpoImage); // expo-image
const StyledBlurView = withUniwind(BlurView); // expo-blur
const StyledMotiView = withUniwind(MotiView); // moti
useResolveClassNames
Tailwind のクラス文字列を React Native のスタイルオブジェクトに変換します。単発のケースや、style のみを受け取るコンポーネントで使用します。
import { useResolveClassNames } from 'uniwind';
const headerStyle = useResolveClassNames('bg-primary p-4');
const cardStyle = useResolveClassNames('bg-card dark:bg-card rounded-lg shadow-sm');
// React Navigation screen options
<Stack.Navigator screenOptions={{ headerStyle, cardStyle }} />
比較
| 機能 | withUniwind | useResolveClassNames |
|---|---|---|
| セットアップ | コンポーネントごとに一度 | 使用するたび |
| パフォーマンス | 最適化済み | やや低速 |
| 最適な用途 | 再利用可能なコンポーネント | 単発の用途、ナビゲーション設定 |
| 構文 | className="..." | style={...} |
動的な ClassNames
絶対にしないでください(Tailwind はビルド時にスキャンします)
// BROKEN — template literal with variable
<View className={`bg-${color}-500`} />
<Text className={`text-${size}`} />
正しいパターン
// Ternary with complete class names
<View className={isActive ? 'bg-primary' : 'bg-muted'} />
// Mapping object
const colorMap = {
primary: 'bg-blue-500 text-white',
danger: 'bg-red-500 text-white',
ghost: 'bg-transparent text-foreground',
};
<Pressable className={colorMap[variant]} />
// Array join for multiple conditions
<View className={[
'p-4 rounded-lg',
isActive && 'bg-primary',
isDisabled && 'opacity-50',
].filter(Boolean).join(' ')} />
tailwind-variants(tv)
バリアントや複合バリアントを使った複雑なコンポーネントのスタイリングに使用します。
import { tv } from 'tailwind-variants';
const button = tv({
base: 'font-semibold rounded-lg px-4 py-2 items-center justify-center',
variants: {
color: {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-500 text-white',
danger: 'bg-red-500 text-white',
ghost: 'bg-transparent text-foreground border border-border',
},
size: {
sm: 'text-sm px-3 py-1.5',
md: 'text-base px-4 py-2',
lg: 'text-lg px-6 py-3',
},
disabled: {
true: 'opacity-50',
},
},
compoundVariants: [
{ color: 'primary', size: 'lg', class: 'bg-blue-600' },
],
defaultVariants: { color: 'primary', size: 'md' },
});
<Pressable className={button({ color: 'primary', size: 'lg' })}>
<Text className="text-white font-semibold">Click</Text>
</Pressable>
cn ユーティリティ — クラスの重複排除
Uniwind は競合する className を自動で重複排除しません。つまり、同じプロパティが複数のクラスに含まれる場合、両方が適用され、結果は予測できません。カスタム CSS クラスと Tailwind ユーティリティを組み合わせる場合は、特に注意が必要です。
セットアップ
npm install tailwind-merge clsx
// lib/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
cn が必要な場合
- className プロパティのマージ — コンポーネントが、競合する可能性のある外部 className を受け取る場合:
import { cn } from '@/lib/cn';
<View className={cn('p-4 bg-white', props.className)} />
<Text className={cn('text-base', isActive && 'text-primary', disabled && 'opacity-50')} />
- 重要:カスタム CSS クラスと Tailwind ユーティリティの併用 — カスタム CSS クラスが設定するプロパティを Tailwind ユーティリティも設定する場合、重複排除のために
cn()を必ず使用してください。
/* global.css */
.card {
background-color: white;
border-radius: 12px;
padding: 16px;
}
// WRONG — both .card (padding: 16px) and p-6 (padding: 24px) apply, result is unpredictable
<View className="card p-6" />
// CORRECT — cn deduplicates, p-6 wins over .card's padding
<View className={cn('card', 'p-6')} />
- tv() の出力と追加クラスの組み合わせ — tv は独自のバリアントを処理しますが、さらにクラスを追加する場合:
<Pressable className={cn(button({ color: 'primary' }), props.className)} />
cn が不要な場合
- 競合のない静的な className:
<View className="flex-1 p-4 bg-white" /> - Tailwind と重複しない単一のカスタム CSS クラス:
<View className="card-shadow mt-4" />(card-shadow がbox-shadowのみを設定し、それを設定する Tailwind クラスがない場合)
重要ユーティリティとスタイルの詳細度
Uniwind は、同じスタイルプロパティに対して別のユーティリティを上書きする必要がある場合に、Tailwind の important 修飾子(!)をサポートします。
import { View, Pressable } from 'react-native';
// bg-red-500! has higher priority than bg-blue-500
<View className="bg-blue-500 bg-red-500!" />;
// Important utilities work with state and platform variants
<Pressable className="bg-blue-500 active:bg-red-500!" />;
<View className="pt-4 ios:pt-12! android:pt-8!" />;
優先順位のルール:
- 重要ユーティリティ(
bg-red-500!)は、同じプロパティに対して重要ではないユーティリティ(bg-blue-500)を上書きします。 - 重要バリアントは通常どおり機能します:
active:bg-red-500!、ios:pt-12!、dark:text-white!。 - インライン
styleは、重要な className ユーティリティよりも常に優先されます:<View className="bg-red-500!" style={{ backgroundColor: 'blue' }} />は青で描画されます。 !は控えめに使用してください。再利用可能なコンポーネントや利用側での上書きには、tailwind-mergeを使ったcn()を推奨します。
テーマ設定
クイックセットアップ(dark: プレフィックス)
設定不要ですぐに動作します。
<View className="bg-white dark:bg-gray-900">
<Text className="text-black dark:text-white">Themed</Text>
</View>
小規模なアプリやプロトタイピングに最適です。カスタムテーマには対応しきれません。
拡張可能なセットアップ(CSS 変数)
global.css で定義すれば、dark: プレフィックスなしでどこでも使用できます。
@layer theme {
:root {
@variant light {
--color-background: #ffffff;
--color-foreground: #111827;
--color-foreground-secondary: #6b7280;
--color-card: #ffffff;
--color-border: #e5e7eb;
--color-muted: #9ca3af;
--color-primary: #3b82f6;
--color-danger: #ef4444;
--color-success: #10b981;
}
@variant dark {
--color-background: #000000;
--color-foreground: #ffffff;
--color-foreground-secondary: #9ca3af;
--color-card: #1f2937;
--color-border: #374151;
--color-muted: #6b7280;
--color-primary: #3b82f6;
--color-danger: #ef4444;
--color-success: #10b981;
}
}
}
// Auto-adapts to current theme — no dark: prefix needed
<View className="bg-card border border-border p-4 rounded-lg">
<Text className="text-foreground text-lg font-bold">Title</Text>
<Text className="text-muted mt-2">Subtitle</Text>
</View>
変数の命名:--color-background → bg-background、text-background。
明示的な dark: バリアントより CSS 変数を優先してください。より簡潔で、保守しやすく、カスタムテーマにも自動で対応できます。
カスタムテーマ
ステップ 1 — global.css で定義します。
@layer theme {
:root {
@variant light { /* ... */ }
@variant dark { /* ... */ }
@variant ocean {
--color-background: #0c4a6e;
--color-foreground: #e0f2fe;
--color-primary: #06b6d4;
--color-card: #0e7490;
--color-border: #155e75;
/* Must define ALL the same variables as light/dark */
}
}
}
ステップ 2 — metro.config.js に登録します(light/dark は自動のため除外します)。
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
extraThemes: ['ocean'],
});
テーマを追加した後は Metro を再起動してください。
ステップ 3 — 使用します。
Uniwind.setTheme('ocean');
テーマ API
import { Uniwind, useUniwind } from 'uniwind';
// Imperative (no re-render)
Uniwind.setTheme('dark'); // Force dark
Uniwind.setTheme('light'); // Force light
Uniwind.setTheme('system'); // Follow device (re-enables adaptive themes)
Uniwind.setTheme('ocean'); // Custom theme (must be in extraThemes)
Uniwind.currentTheme; // Current theme name
Uniwind.hasAdaptiveThemes; // true if following system
// Reactive hook (re-renders on change)
const { theme, hasAdaptiveThemes } = useUniwind();
Uniwind.setTheme('light') / setTheme('dark') は、ネイティブコンポーネント(Alert、Modal、システムダイアログ)を同期するために、Appearance.setColorScheme も呼び出します。
デフォルトでは、Uniwind は「system」テーマを使用し、デバイスのカラースキームに従います。テーマを上書きするには、希望するテーマを指定して Uniwind.setTheme を呼び出します。実行時のテーマ切り替えを避けるため、React コンポーネントの外部で呼び出すこともできます。
テーマ切り替えの例
import { View, Pressable, Text, ScrollView } from 'react-native';
import { Uniwind, useUniwind } from 'uniwind';
export const ThemeSwitcher = () => {
const { theme, hasAdaptiveThemes } = useUniwind();
const activeTheme = hasAdaptiveThemes ? 'system' : theme;
const themes = [
{ name: 'light', label: 'Light' },
{ name: 'dark', label: 'Dark' },
{ name: 'system', label: 'System' },
];
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className="flex-row gap-2 p-4">
{themes.map((t) => (
<Pressable
key={t.name}
onPress={() => Uniwind.setTheme(t.name)}
className={`px-4 py-3 rounded-lg items-center ${
activeTheme === t.name ? 'bg-primary' : 'bg-card border border-border'
}`}
>
<Text className={`text-sm ${
activeTheme === t.name ? 'text-white' : 'text-foreground'
}`}>
{t.label}
</Text>
</Pressable>
))}
</View>
</ScrollView>
);
};
ScopedTheme
グローバルテーマを変更せずに、サブツリーに別のテーマを適用します。
import { ScopedTheme } from 'uniwind';
<View className="gap-3">
<PreviewCard />
<ScopedTheme theme="dark">
<PreviewCard /> {/* Renders with dark theme */}
</ScopedTheme>
<ScopedTheme theme="ocean">
<PreviewCard /> {/* Renders with ocean theme */}
</ScopedTheme>
</View>
- 最も近い
ScopedThemeが優先されます(スコープのネストに対応しています) - フック(
useUniwind、useResolveClassNames、useCSSVariable)は、最も近いスコープのテーマを基準に解決します - スコープ内の
withUniwindでラップされたコンポーネントも、スコープのテーマ値を解決します - カスタムテーマは
extraThemesに登録する必要があります
useCSSVariable
JavaScript で CSS 変数の値にアクセスします。
import { useCSSVariable } from 'uniwind';
const primaryColor = useCSSVariable('--color-primary');
const spacing = useCSSVariable('--spacing-4');
// Multiple variables at once
const [bg, fg] = useCSSVariable(['--color-background', '--color-foreground']) as [string, string]
アニメーション、チャートライブラリ、サードパーティ製コンポーネントの設定、デザイントークンを使った計算に使用します。
useCSSVariable の結果は string | number | undefined を返す可能性があるため、型をキャストする必要があります。
Uniwind は指定された変数が存在するかどうかや、その型を把握していないため、ユニオン型を返します。
getCSSVariable
React の外部(イベントハンドラー、非同期コールバック、ユーティリティモジュール、ワークレット)で CSS 変数の値を読み取ります。Uniwind 1.6.4+ で利用できます。
import { Uniwind } from 'uniwind';
const primary = Uniwind.getCSSVariable('--color-primary');
const [bg, fg] = Uniwind.getCSSVariable(['--color-background', '--color-foreground']) as [string, string];
値に関する規則は useCSSVariable と同じです(変数は className で使用するか、@theme static で宣言する必要があります)。戻り値の型も同じく string | number | undefined です。必要に応じてキャストしてください。
リアクティブではなく、値は一度だけ読み取られます。コンポーネント内でリアクティブな値を使う場合は useCSSVariable を使用します。getCSSVariable は、単発の読み取り(onPress ハンドラー、ユーティリティ関数、ネイティブモジュールの設定)に使用してください。
実行時の CSS 変数の更新
実行時にテーマ変数を更新します(例:ユーザーが選択したブランドカラーや、API によって決まるテーマ)。
Uniwind.updateCSSVariables('light', {
'--color-primary': '#ff6600',
'--color-background': '#fafafa',
});
更新はテーマごとに適用され、すぐに反映されます。
@theme static
classNames では使用しない、JavaScript 専用の値に使用します。
@theme static {
--chart-line-width: 2;
--chart-dot-radius: 4;
--animation-duration: 300;
}
useCSSVariable('--chart-line-width') でアクセスします。チャートの設定、アニメーションの時間、ネイティブモジュールの値に使用します。
OKLCH カラーのサポート
知覚的に均一なカラー形式です。より広い色域と、一貫した明度を実現します。
@layer theme {
:root {
@variant light {
--color-primary: oklch(0.5 0.2 240);
--color-background: oklch(1 0 0);
}
@variant dark {
--color-primary: oklch(0.6 0.2 240);
--color-background: oklch(0.13 0.004 17.69);
}
}
}
Display P3 カラーのサポート
P3 色空間に対応したデバイス(最新の iPhone や Mac のほとんど)向けの広色域カラー形式です。Uniwind は color(display-p3 ...) の値を解析し、ネイティブで使用できるように変換します。
@layer theme {
:root {
@variant light {
--color-primary: color(display-p3 0.2 0.4 1);
--color-accent: color(display-p3 1 0.3 0.3);
}
@variant dark {
--color-primary: color(display-p3 0.3 0.5 1);
--color-accent: color(display-p3 1 0.4 0.4);
}
}
}
プラットフォームセレクター
className でプラットフォーム固有のスタイルを直接適用します。
// Individual platforms
<View className="ios:bg-red-500 android:bg-blue-500 web:bg-green-500" />
// native: shorthand (iOS + Android)
<View className="native:bg-blue-500 web:bg-gray-500" />
// TV platforms
<View className="tv:p-8 android-tv:bg-black apple-tv:bg-gray-900" />
// Combine with other utilities
<View className="p-4 ios:pt-12 android:pt-6 web:pt-4" />
グローバル値には、@layer theme 内でプラットフォームバリアントを使用します(@media ではなく @variant を使用してください)。
@layer theme {
:root {
@variant ios { --font-sans: 'SF Pro Text'; }
@variant android { --font-sans: 'Roboto-Regular'; }
@variant web { --font-sans: 'Inter'; }
}
}
Platform.select() よりプラットフォームセレクターを優先してください。構文がすっきりし、インポートも不要です。
データセレクター
data-[prop=value]:utility を使って、プロパティの値に基づいてスタイルを設定します。
// Boolean props
<Pressable
data-selected={isSelected}
className="border rounded px-3 py-2 data-[selected=true]:ring-2 data-[selected=true]:ring-primary"
/>
// String props
<View
data-state={isOpen ? 'open' : 'closed'}
className="p-4 data-[state=open]:bg-muted/50 data-[state=closed]:bg-transparent"
/>
// Tabs pattern
<Pressable
data-selected={route.key === current}
className="px-4 py-2 rounded-md text-foreground/60
data-[selected=true]:bg-primary data-[selected=true]:text-white"
>
<Text>{route.title}</Text>
</Pressable>
// Toggle pattern
<Pressable
data-checked={enabled}
className="h-6 w-10 rounded-full bg-muted data-[checked=true]:bg-primary"
>
<View className="h-5 w-5 rounded-full bg-background translate-x-0 data-[checked=true]:translate-x-4" />
</Pressable>
ルール:
- 等価セレクター(
data-[prop=value])のみサポートされます - 存在のみを確認するセレクター(
data-[prop])はサポートされていません has-data-*親セレクターは React Native ではサポートされていません- ブール値は、ブール値形式と文字列形式の両方に一致します
インタラクティブ状態
// active: — when pressed
<Pressable className="bg-primary active:bg-primary/80 active:opacity-90 active:scale-95">
<Text className="text-white">Press me</Text>
</Pressable>
// disabled: — when disabled prop is true
<Pressable
disabled={isLoading}
className="bg-primary disabled:bg-gray-300 disabled:opacity-50"
>
<Text className="text-white disabled:text-gray-500">Submit</Text>
</Pressable>
// focus: — keyboard/accessibility focus
<TextInput
className="border border-border rounded-lg px-4 py-2 focus:border-primary focus:ring-2 focus:ring-primary/20"
/>
<Pressable className="bg-card rounded-lg p-4 focus:ring-2 focus:ring-primary">
<Text className="text-foreground">Focusable</Text>
</Pressable>
状態に対応するコンポーネント:
- Pressable:
active:、disabled:、focus: - TextInput:
active:、disabled:、focus: - Switch:
disabled: - Text:
active:、disabled: - TouchableOpacity / TouchableHighlight / TouchableNativeFeedback / TouchableWithoutFeedback:
active:、disabled:
レスポンシブブレークポイント
モバイルファーストです。プレフィックスなしのスタイルはすべてのサイズに適用され、プレフィックス付きのスタイルはそのブレークポイント以上で適用されます。
| Prefix | Min Width | Typical Device |
|---|---|---|
| (none) | 0px | すべて(モバイル) |
sm: | 640px | 大型スマートフォン |
md: | 768px | タブレット |
lg: | 1024px | 横向きのタブレット |
xl: | 1280px | デスクトップ |
2xl: | 1536px | 大型デスクトップ |
// Responsive padding and typography
<View className="p-4 sm:p-6 lg:p-8">
<Text className="text-base sm:text-lg lg:text-xl font-bold">Responsive</Text>
</View>
// Responsive grid (1 col → 2 col → 3 col)
<View className="flex-row flex-wrap">
<View className="w-full sm:w-1/2 lg:w-1/3 p-2">
<View className="bg-card p-4 rounded"><Text>Item</Text></View>
</View>
</View>
// Responsive visibility
<View className="hidden sm:flex flex-row gap-4">
<Text>Visible on tablet+</Text>
</View>
<View className="flex sm:hidden">
<Text>Mobile only</Text>
</View>
カスタムブレークポイント:
@theme {
--breakpoint-xs: 480px;
--breakpoint-tablet: 820px;
--breakpoint-3xl: 1920px;
}
使用例:xs:p-2 tablet:p-4 3xl:p-8
モバイルファーストで設計します — ベーススタイル(プレフィックスなし)から始め、ブレークポイントで拡張します。
// CORRECT — mobile-first
<View className="w-full sm:w-3/4 md:w-1/2 lg:w-1/3" />
// WRONG — desktop-first (reversed order is confusing and fragile)
<View className="w-full lg:w-1/2 md:w-3/4 sm:w-full" />
セーフエリアユーティリティ
パディング
| Class | Description |
|---|---|
p-safe | 全辺 |
pt-safe / pb-safe / pl-safe / pr-safe | 個別の辺 |
px-safe / py-safe | 水平方向/垂直方向 |
マージン
| Class | Description |
|---|---|
m-safe | 全辺 |
mt-safe / mb-safe / ml-safe / mr-safe | 個別の辺 |
mx-safe / my-safe | 水平方向/垂直方向 |
位置指定
| Class | Description |
|---|---|
inset-safe | 全辺 |
top-safe / bottom-safe / left-safe / right-safe | 個別の辺 |
x-safe / y-safe | 水平方向/垂直方向のインセット |
複合バリアント
| Pattern | Behavior | Example |
|---|---|---|
{prop}-safe-or-{value} | Math.max(inset, value) — 最小間隔を確保します | pt-safe-or-4 |
{prop}-safe-offset-{value} | inset + value — インセットに間隔を追加します | pb-safe-offset-4 |
セットアップ
Uniwind Free(デフォルト) — インセットを更新するには、react-native-safe-area-context が必要です。
App コンポーネントを SafeAreaProvider と SafeAreaListener でラップし、onChange コールバックで Uniwind.updateInsets(insets) を呼び出します。
import { SafeAreaProvider, SafeAreaListener } from 'react-native-safe-area-context';
import { Uniwind } from 'uniwind';
export default function App() {
return (
<SafeAreaProvider>
<SafeAreaListener
onChange={({ insets }) => {
Uniwind.updateInsets(insets);
}}
>
<View className="pt-safe px-safe">{/* content */}</View>
</SafeAreaListener>
</SafeAreaProvider>
);
}
Uniwind Pro — 自動で適用されるため、セットアップは不要です。インセットはネイティブレイヤーから注入されます。
CSS 関数
Uniwind は、デバイスやテーマに応じたスタイル設定のための CSS 関数を提供します。これらはどこでも(カスタム CSS クラス、@utility など)使用できます。ただし、@theme {} 内では使用できません(静的な値のみ受け付けます)。再利用可能な Tailwind スタイルのユーティリティクラスを作成するには、@utility を使用します。
hairlineWidth()
デバイスで表示可能な最も細い線幅を返します。控えめなボーダーや区切り線に使用します。
@utility h-hairline { height: hairlineWidth(); }
@utility border-hairline { border-width: hairlineWidth(); }
@utility w-hairline { width: calc(hairlineWidth() * 10); }
<View className="h-hairline bg-gray-300" />
<View className="border-hairline border-gray-200 rounded-lg p-4" />
fontScale(multiplier?)
デバイスのフォントスケールのアクセシビリティ設定に基づいて、基準値に倍率を掛けます。ユーザーが設定した大きめ、または小さめのテキストサイズを反映します。
fontScale()— 倍率 1 を使用します(デバイスのフォントスケール × 1)fontScale(0.9)— 小さめのスケールfontScale(1.2)— 大きめのスケール
@utility text-sm-scaled { font-size: fontScale(0.9); }
@utility text-base-scaled { font-size: fontScale(); }
@utility text-lg-scaled { font-size: fontScale(1.2); }
<Text className="text-sm-scaled text-gray-600">Small accessible text</Text>
<Text className="text-base-scaled">Regular accessible text</Text>
pixelRatio(multiplier?)
デバイスのピクセル比に基づいて、値に倍率を掛けます。画面密度に応じて拡大縮小する、ピクセル単位で正確なデザインを作成できます。
pixelRatio()— 倍率 1 を使用します(デバイスのピクセル比 × 1)pixelRatio(2)— ピクセル比を 2 倍にします
@utility w-icon { width: pixelRatio(); }
@utility w-avatar { width: pixelRatio(2); }
<Image source={{ uri: 'avatar.png' }} className="w-avatar rounded-full" />
light-dark(lightValue, darkValue)
現在のテーマモードに応じて異なる値を返します。テーマが変わると自動的に切り替わるため、手動で切り替えるロジックは不要です。
- 第 1 引数:ライトテーマ用の値
- 第 2 引数:ダークテーマ用の値
@utility bg-adaptive { background-color: light-dark(#ffffff, #1f2937); }
@utility text-adaptive { color: light-dark(#111827, #f9fafb); }
@utility border-adaptive { border-color: light-dark(#e5e7eb, #374151); }
<View className="bg-adaptive border-adaptive border rounded-lg p-4">
<Text className="text-adaptive">Adapts to light/dark theme</Text>
</View>
カスタム CSS クラスでも動作します(@utility 内だけではありません)。
.adaptive-card {
background-color: light-dark(#ffffff, #1f2937);
color: light-dark(#111827, #f9fafb);
}
カスタム CSS とユーティリティ
カスタム CSS クラス
Uniwind は、global.css で定義されたカスタム CSS クラス名をサポートします。これらはビルド時にコンパイルされるため、実行時のオーバーヘッドはありません。Tailwind ユーティリティでは表現しにくいスタイル(複雑なボックスシャドウや、複数のプロパティをまとめたスタイルなど)に使用します。
/* global.css */
.card-shadow {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.adaptive-surface {
background-color: light-dark(#ffffff, #1f2937);
color: light-dark(#111827, #f9fafb);
}
.container {
flex: 1;
width: 100%;
max-width: 1200px;
}
他の Tailwind クラスと同様に、className で適用します。
<View className="card-shadow" />
カスタム CSS と Tailwind の併用
1 つの className で、カスタム CSS クラスと Tailwind ユーティリティを組み合わせられます。
<View className="card-shadow p-4 m-2">
<Text className="adaptive-surface mb-2">{title}</Text>
<View className="container flex-row">{children}</View>
</View>
警告:カスタム CSS クラスと Tailwind ユーティリティが同じプロパティを設定する場合は、重複を解消するために必ず cn() を使用してください。cn() を使用しないと、両方の値が適用され、結果が予測できなくなります。
// WRONG — .container sets flex:1, and flex-1 also sets flex:1 (harmless but wasteful)
// WRONG — .container sets width:100%, and w-full also sets width:100% (redundant)
// DANGEROUS — .card-shadow sets border-radius:12px, and rounded-2xl sets border-radius:16px — CONFLICT!
<View className="card-shadow rounded-2xl" />
// CORRECT — cn ensures rounded-2xl wins
import { cn } from '@/lib/cn';
<View className={cn('card-shadow', 'rounded-2xl')} />
経験則:カスタム CSS クラスが、併用する Tailwind ユーティリティと重複する可能性のあるプロパティを設定する場合は、必ず cn() で囲んでください。設定方法の詳細は、cn ユーティリティのセクションを参照してください。
カスタム CSS のガイドライン
- セレクターはフラットに保ち、深いネストや子セレクターは使用しないでください
- 単一プロパティのシンプルなスタイルには、Tailwind ユーティリティを優先してください
classNameで冗長になる複雑なスタイルや複数プロパティのまとまりには、カスタムクラスを使用してください- テーマに対応するカスタムクラスには、
light-dark()を使用してください - カスタムクラスは、Tailwind の命名規則に合わない共通のデザイントークン(例:
.card、.chip、.badge-dot)に最適です
カスタムユーティリティ(@utility)
@utility ディレクティブを使うと、組み込みの Tailwind クラスとまったく同じように機能するユーティリティクラスを作成できます。主な用途は 3 つあります。
1. 変数駆動型ユーティリティ(実行時に値を注入)
updateCSSVariables を使って実行時に注入する CSS 変数から値を取得するユーティリティを作成します。変数が更新される前から Uniwind が追跡できるように、@theme static を使って変数を宣言します。
/* global.css */
@theme static {
--header-height: 0px;
}
@utility p-safe-header {
padding-top: var(--header-height);
}
実際の値は実行時に注入します(例:react-navigation のレイアウトイベントから)。
import { Uniwind } from 'uniwind'
// e.g., inside a navigation layout listener
Uniwind.updateCSSVariables(Uniwind.currentTheme, {
'--header-height': headerHeight,
})
<View className="p-safe-header flex-1" />
2. 新しいユーティリティ(Tailwind に相当するものがない場合)
組み込みの Tailwind クラスがないスタイルに使用します。
@utility h-hairline { height: hairlineWidth(); }
@utility text-scaled { font-size: fontScale(); }
@utility card-shadow {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
ほかの Tailwind クラスと同様に使用できます:<View className="h-hairline card-shadow" />
3. 既存の Tailwind ユーティリティを上書き
@utility を使って、組み込みクラスの動作を完全に置き換えられます。例:border が常に --color-primary を使用するようにします。
@utility border {
border-width: 1px;
border-style: solid;
border-color: var(--color-primary);
}
@theme ディレクティブ
global.css で Tailwind のデザイントークンをカスタマイズします。
@theme {
/* Colors */
--color-primary: #3b82f6;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a8a;
/* Typography */
--font-sans: 'Roboto-Regular';
--font-sans-medium: 'Roboto-Medium';
--font-sans-bold: 'Roboto-Bold';
--font-mono: 'FiraCode-Regular';
/* Spacing & sizing */
--text-base: 15px;
--spacing-4: 16px;
--radius-lg: 12px;
/* Breakpoints */
--breakpoint-tablet: 820px;
}
使用例:bg-brand-500、text-brand-900、font-sans、font-mono、rounded-lg。
フォント
React Native では、ファミリーごとに単一のフォントが必要です。フォールバックは使用できません。
@theme {
--font-sans: 'Roboto-Regular';
--font-sans-bold: 'Roboto-Bold';
--font-mono: 'FiraCode-Regular';
}
フォント名は、拡張子を除いたフォントファイル名と完全に一致している必要があります。
Expo:expo-font プラグインを使って app.json でフォントを設定し、CSS で参照します。
Bare RN:react-native-asset を使ってフォントをリンクし、同じ CSS 設定を使用します。
プラットフォーム固有のフォント(@media ではなく @variant を使用):
@layer theme {
:root {
@variant ios { --font-sans: 'SF Pro Text'; }
@variant android { --font-sans: 'Roboto-Regular'; }
@variant web { --font-sans: 'system-ui'; }
}
}
グラデーション
組み込みでサポートされているため、追加の依存関係は不要です。
<View className="bg-gradient-to-r from-red-500 via-yellow-500 to-green-500 p-4 rounded-lg">
<Text className="text-white font-bold">Gradient</Text>
</View>
expo-linear-gradient の場合は、withUniwind でラップすると、className ベースのレイアウトやスタイリング(パディング、border-radius、flex など)を適用できます。ただし、colors プロパティは配列であり、className では解決できないため、明示的に指定する必要があります。テーマに対応する色を取得するには、useCSSVariable を使用してください。
import { useCSSVariable } from 'uniwind';
import { withUniwind } from 'uniwind';
import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
const LinearGradient = withUniwind(RNLinearGradient);
function GradientCard() {
// useCSSVariable returns string | number | undefined
const primary = useCSSVariable('--color-primary');
const secondary = useCSSVariable('--color-secondary');
// Guard against undefined — LinearGradient.colors requires valid ColorValues
if (!primary || !secondary) {
return null;
}
return (
<LinearGradient
className="flex-1 rounded-2xl p-6"
colors={[primary as string, secondary as string]}
>
<Text className="text-white font-bold">Themed gradient</Text>
</LinearGradient>
);
}
または、再利用できるように、共有モジュールからラップ済みコンポーネントをエクスポートします。
// components/styled.ts
import { withUniwind } from 'uniwind';
import { LinearGradient as RNLinearGradient } from 'expo-linear-gradient';
export const LinearGradient = withUniwind(RNLinearGradient);
// usage — className handles layout, colors still passed manually
import { LinearGradient } from '@/components/styled';
<LinearGradient className="rounded-xl p-4" colors={['#ff6b6b', '#4ecdc4']}>
<Text className="text-white">Static gradient</Text>
</LinearGradient>
React Navigation との統合
style オブジェクトのみを受け付ける画面オプションには、useResolveClassNames を使用します。
import { useResolveClassNames } from 'uniwind';
function Layout() {
const headerStyle = useResolveClassNames('bg-background');
const headerTitleStyle = useResolveClassNames('text-foreground font-bold');
return (
<Stack.Navigator
screenOptions={{
headerStyle,
headerTitleStyle,
}}
/>
);
}
すでに React Navigation の <ThemeProvider> を使用している場合は、そのまま使用してください。ナビゲーション固有のテーマ設定を管理します。
UI キットとの互換性
- HeroUI Native:Uniwind と連携します。内部では
tailwind-variants(tv)を使用しています。HeroUI コンポーネントにclassNameを直接適用してください。Bun ユーザー向け:Bun はnode_modulesにシンボリックリンクを使用するため、本番ビルドで Tailwind の Oxide スキャナーがライブラリのクラスを検出できないことがあります。対処法:解決済みのパスを@sourceで使用し、パッケージをホイストします。@source "../../node_modules/heroui-native/lib";# .npmrc public-hoist-pattern[]=heroui-native - react-native-reusables:互換性があります。
- Gluestack v4.1+:互換性があります。
- Lucide React Native:アイコンの色には、
colorClassName="accent-blue-500"を指定してwithUniwind(LucideIcon)を使用します。すべての Lucide アイコンで機能します。 - @shopify/flash-list:
classNameとcontentContainerClassNameをサポートするには、withUniwind(FlashList)を使用します。注:withUniwindはrefのジェネリック型パラメーターを引き継ぎません。必要に応じて、手動でキャストしてください。
UI キット間で一貫したテーマを保つには、セマンティックなカラートークン(bg-primary、text-foreground)を使用してください。
サポート対象クラスと非サポートクラス
React Native は Yoga レイアウトエンジンを使用します。Web CSS との主な違いは次のとおりです。
- CSS のカスケード/継承はありません — スタイルは親から継承されません
- デフォルトで Flexbox — すべてのビューで
flexDirection: 'column'の Flexbox が使用されます - CSS プロパティは限定的です — float、grid、疑似要素は使用できません
組み込みの追加ユーティリティ
Uniwind は、標準の Tailwind では対応していない React Native の機能向けに、追加のユーティリティクラスを提供します。
| クラス | 効果 |
|---|---|
border-continuous | borderCurve: 'continuous' を設定します — 滑らかなスーパー楕円形の角(iOS) |
border-circular | borderCurve: 'circular' を設定します — 標準的な円形の角(iOS のデフォルト) |
// Smooth iOS-style rounded corners (like SwiftUI's .continuous)
<View className="rounded-2xl border-continuous bg-card p-4">
<Text className="text-foreground">Smooth corners</Text>
</View>
サポート対象(標準の Tailwind 全般)
レイアウト、余白、サイズ、タイポグラフィ、色、ボーダー、エフェクト、Flexbox、配置、変形、インタラクティブな状態。
非サポート(Web 固有のため、無視されます)
hover:visited:— 代わりに Pressable のactive:を使用しますbefore:after:placeholder:— 疑似要素float-*clear-*columns-*print:screen:break-before-*break-after-*break-inside-*
Uniwind Pro
API 互換性 100% の有料アップグレードです。最高のパフォーマンスを求めるアプリ向けに、第 2 世代の C++ エンジンを基盤としています。段階制料金(年払い):$99/シート(1~3)、$49(4~6)、$29(7~15)、$1(16 以上)。料金とライセンス:https://uniwind.dev/pricing
料金とライセンス
- 段階制のシート単価(年払い。該当する場合を除き VAT は別途):シート 1~3 は $99、4~6 は $49、7~15 は $29、16 以上は $1
- 個人ライセンス:エンジニアごとの個人用 Pro ライセンス
- チームライセンス:一元的なキー管理 — メンバーをすぐに追加または削除できます
- CI/CD ライセンス:自動ビルド環境とヘッドレスビルド環境を全面的にサポートします
- エンタープライズ:カスタムプランをご利用いただけます([email protected] までお問い合わせください)
- 優先サポート:重大な問題には優先対応します
概要
- C++ スタイルエンジン:第 2 世代 Unistyles C++ エンジンを基盤としています。React の再レンダリングを発生させずにスタイルを ShadowTree に直接注入し、classNames とネイティブ層を最適化された直通経路で結びます
- パフォーマンス:ベンチマーク結果は約 55ms(StyleSheet は 49ms、従来の Uniwind は 81ms、NativeWind は 197ms)で、ネイティブに近い速度です
- 20 個のコンポーネントで、55 個以上の
classNameprops を再レンダリングなしで更新できます(上記に挙げたすべてのコンポーネントバインディング) - Reanimated アニメーション:
classNameを介してanimate-*とtransition-*を使用できます(Reanimated v4) - ネイティブのインセットと実行時の値:セーフエリア、デバイスの回転、フォントサイズの更新を自動で反映します —
SafeAreaListenerの設定は不要です - テーマの遷移:テーマ切り替え時にネイティブのアニメーション付き遷移を行います(フェード、スライド、円形マスク)
- グループバリアント:Tailwind の
group-active:*/group-focus:*は、再レンダリングを発生させずに、親のインタラクション状態を C++ のシャドウツリー経由で伝播します - デフォルトスタイル:CSS セレクター
View { ... }やText { ... }からデフォルトの React Native コンポーネントをスタイリングできる、実験的な1.2.0+の機能です - 優先サポート:技術的な課題によってチームの進行が妨げられないようにします
package.json のパッケージ指定:"uniwind": "npm:uniwind-pro@latest"。
インストール
-
package.jsonで依存関係のエイリアスを設定します:{ "dependencies": { "uniwind": "npm:uniwind-pro@latest" } } -
ピア依存関係をインストールします:
npm install react-native-nitro-modules react-native-reanimated react-native-worklets -
認証します:
npx uniwind-pro(対話形式で「Login with GitHub」を選択します) -
Babel プラグインを追加します — 既存の
plugins配列にreact-native-worklets/pluginを追加してください。プリセットは置き換えないでください(Expo ではbabel-preset-expoを、bare-RN では既存のプリセットを維持します):// babel.config.js module.exports = { // your existing presets and config — leave untouched plugins: [ // ...other plugins 'react-native-worklets/plugin', ], };すでに Reanimated を使用している場合は、worklets プラグインが設定済みの可能性があります。
-
必要に応じて postinstall を許可リストに追加します:
- bun:
package.jsonに"trustedDependencies": ["uniwind"]を追加します - yarn v2+:
.yarnrc.ymlで設定します - pnpm:
pnpm config set enable-pre-post-scripts true
- bun:
-
ネイティブアプリを再ビルドします:
npx expo prebuild --clean && npx expo run:ios
Pro は Expo Go では動作しません。ネイティブの再ビルドが必要です。
インストールを確認する:node_modules/uniwind内のネイティブモジュール(.cpp、.mmファイル)を確認します。
Reanimated アニメーション(Reanimated v4.0.0 以降が必要)
<View className="size-32 bg-primary rounded animate-spin" />
<View className="size-32 bg-primary rounded animate-bounce" />
<View className="size-32 bg-primary rounded animate-pulse" />
<View className="size-32 bg-primary rounded animate-ping" />
// Loading spinner
<View className="size-8 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin" />
Tailwind のデフォルトを超えるカスタムキーフレームアニメーション:
| クラス | 説明 |
|---|---|
animate-wiggle | 回転する小刻みな揺れ |
animate-shake | 水平方向の揺れ |
animate-flash | 不透明度が点滅 |
animate-rubber-band | 弾むように伸縮 |
animate-swing | 振り子のような揺れ |
animate-tada | 注目を引く拡大と回転 |
animate-heartbeat | 2回の脈動を繰り返す鼓動 |
animate-jello | 回転しながらゼリーのように揺れる |
animate-float | ゆるやかに上下に浮遊 |
animate-breathe | さりげない呼吸のような拡大縮小 |
animate-tilt | 左右に交互に傾く回転 |
animate-glitch | 素早い水平方向の揺れ |
アニメーションクラスが検出されると、コンポーネントは自動的に Animated バージョンに切り替わります:
| コンポーネント | Animated バージョン |
|---|---|
View | Animated.View |
Text | Animated.Text |
Image | Animated.Image |
ImageBackground | Animated.ImageBackground |
ScrollView | Animated.ScrollView |
FlatList | Animated.FlatList |
TextInput | Animated.TextInput |
Pressable | Animated.Pressable |
Entering と Exiting のアニメーション
className を使って Reanimated の Entering/Exiting アニメーションを適用できます。Reanimated のインポートは不要です。uw-* クラスが検出されると、コンポーネントは自動的にアップグレードされます。
// Bounce in, bounce out
{visible && <View className="size-20 bg-primary rounded-xl uw-entering-bounce-in uw-exiting-bounce-out" />}
// Fade in slowly (1000ms)
{visible && <View className="size-20 bg-primary rounded-xl uw-entering-fade-in uw-entering-duration-1000 uw-exiting-fade-out" />}
Entering プリセット: uw-entering-fade-in uw-entering-fade-in-right uw-entering-fade-in-left uw-entering-fade-in-up uw-entering-fade-in-down uw-entering-slide-in-right uw-entering-slide-in-left uw-entering-slide-in-up uw-entering-slide-in-down uw-entering-zoom-in uw-entering-zoom-in-rotate uw-entering-zoom-in-left uw-entering-zoom-in-right uw-entering-zoom-in-up uw-entering-zoom-in-down uw-entering-zoom-in-easy-up uw-entering-zoom-in-easy-down uw-entering-bounce-in uw-entering-bounce-in-down uw-entering-bounce-in-up uw-entering-bounce-in-left uw-entering-bounce-in-right uw-entering-flip-in-x-up uw-entering-flip-in-x-down uw-entering-flip-in-y-left uw-entering-flip-in-y-right uw-entering-flip-in-easy-x uw-entering-flip-in-easy-y uw-entering-stretch-in-x uw-entering-stretch-in-y uw-entering-rotate-in-down-left uw-entering-rotate-in-down-right uw-entering-rotate-in-up-left uw-entering-rotate-in-up-right uw-entering-roll-in-left uw-entering-roll-in-right uw-entering-pinwheel-in uw-entering-light-speed-in-right uw-entering-light-speed-in-left
Exiting プリセット: uw-exiting-fade-out uw-exiting-fade-out-right uw-exiting-fade-out-left uw-exiting-fade-out-up uw-exiting-fade-out-down uw-exiting-slide-out-right uw-exiting-slide-out-left uw-exiting-slide-out-up uw-exiting-slide-out-down uw-exiting-zoom-out uw-exiting-zoom-out-rotate uw-exiting-zoom-out-left uw-exiting-zoom-out-right uw-exiting-zoom-out-up uw-exiting-zoom-out-down uw-exiting-zoom-out-easy-up uw-exiting-zoom-out-easy-down uw-exiting-bounce-out uw-exiting-bounce-out-down uw-exiting-bounce-out-up uw-exiting-bounce-out-left uw-exiting-bounce-out-right uw-exiting-flip-out-x-up uw-exiting-flip-out-x-down uw-exiting-flip-out-y-left uw-exiting-flip-out-y-right uw-exiting-flip-out-easy-x uw-exiting-flip-out-easy-y uw-exiting-stretch-out-x uw-exiting-stretch-out-y uw-exiting-rotate-out-down-left uw-exiting-rotate-out-down-right uw-exiting-rotate-out-up-left uw-exiting-rotate-out-up-right uw-exiting-roll-out-left uw-exiting-roll-out-right uw-exiting-pinwheel-out uw-exiting-light-speed-out-right uw-exiting-light-speed-out-left
アニメーション修飾子(パターン:uw-{entering|exiting|layout}-{modifier}):
- Duration:
uw-{type}-duration-75uw-{type}-duration-100...uw-{type}-duration-1000または任意のuw-{type}-duration-{ms} - Delay:
uw-{type}-delay-75...uw-{type}-delay-1000または任意のuw-{type}-delay-{ms} - Easing:
uw-{type}-ease-linearuw-{type}-ease-inuw-{type}-ease-outuw-{type}-ease-in-outuw-{type}-ease-bounce - Spring:
uw-{type}-springifyuw-{type}-damping-{value}uw-{type}-stiffness-{value}uw-{type}-mass-{value}
レイアウトトランジション
兄弟要素の追加または削除時に、位置やサイズの変更をアニメーション化します。
<View className="w-full gap-2">
{items.map(item => (
<View key={item.id} className={`h-14 ${item.color} rounded-xl uw-entering-fade-in uw-exiting-fade-out uw-layout-linear-transition`} />
))}
</View>
| クラス | 説明 |
|---|---|
uw-layout-linear-transition | 滑らかな線形の位置変更 |
uw-layout-fading-transition | 位置変更中にフェード |
uw-layout-jumping-transition | 新しい位置へ弾むように移動 |
uw-layout-curved-transition | 曲線に沿った位置変更 |
uw-layout-sequenced-transition | 順序に沿った位置変更 |
uw-layout-entry-exit-transition | レイアウト変更時の入場/退場を組み合わせる |
トランジション
className または state が変化したときに、プロパティを滑らかに変更します。
// 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>
| クラス | プロパティ |
|---|---|
transition-none | トランジションなし |
transition-all | すべてのプロパティ |
transition-colors | 背景色、ボーダー色、テキスト色 |
transition-opacity | 不透明度 |
transition-shadow | ボックスシャドウ |
transition-transform | 拡大縮小、回転、移動 |
期間:duration-75 duration-100 duration-150 duration-200 duration-300 duration-500 duration-700 duration-1000
イージング:ease-linear ease-in ease-out ease-in-out
遅延:delay-75 delay-100 delay-150 delay-200 delay-300 delay-500 delay-700 delay-1000
Reanimated を直接使用する
Uniwind クラスと併用しても動作します。
import Animated, { FadeIn, FlipInXUp, LinearTransition } from 'react-native-reanimated';
<Animated.Text entering={FadeIn.delay(500)} className="text-foreground text-lg">
Fading in
</Animated.Text>
<Animated.FlatList
data={data}
className="flex-none"
contentContainerClassName="px-2"
layout={LinearTransition}
renderItem={({ item }) => (
<Animated.Text entering={FlipInXUp} className="text-foreground py-2">
{item}
</Animated.Text>
)}
/>
シャドウツリーの更新
コードを変更する必要はありません。props が C++ エンジンに直接接続されるため、再レンダリングが自動的に排除されます。
シャドウツリーの診断
C++ エンジンの動作を観察するための開発専用 API です。コンポーネントがシャドウツリーに登録/登録解除されるタイミングや、スタイルがどのように反映されるかを確認できます。uniwind/diagnostics からインポートします。
import { enableDiagnostics } from 'uniwind/diagnostics';
enableDiagnostics({
reportMounts: true, // log when components register with the shadow tree
reportUnmounts: true, // log when components unregister
reportUpdates: true, // log style updates with property-level detail
});
3 つのオプションはすべてデフォルトで false です。必要なものだけを有効にしてください(複雑な画面ですべてをログに記録すると、ログが大量になります)。更新ログは 🔥 C++ updates(シャドウツリーを介して適用)と ✨ Native updates(ネイティブビューに直接設定される props)にグループ化されます。どちらも React の再レンダリングなしで発生します。
用途:テーマ切り替えのデバッグ(reportUpdates)、メモリリークの検出(画面遷移後にマウント/アンマウント数がゼロに戻ることを確認)、再レンダリングがゼロであることの検証。
プラットフォーム対応状況:iOS と Android ではすべての診断機能を利用できます。Web では enableDiagnostics は何も出力しないスタブです。
グループバリアント
Tailwind の group バリアントは、C++ シャドウツリーを通じて、親のインタラクション状態を子孫に伝播します。再レンダリングもコンテキストプロバイダーも必要ありません。
// Basic group — descendants react to parent press
<Pressable className="group p-4 bg-base rounded-xl">
<Text className="text-default group-active:text-primary">Press the card</Text>
<View className="size-8 bg-blue-500 rounded group-active:bg-red-500" />
</Pressable>
// Named groups — descendants pick which ancestor to follow
<Pressable className="group/card p-4 bg-base rounded-xl">
<Pressable className="group/button px-3 py-1 bg-primary rounded">
<Text className="text-white group-active/card:opacity-50 group-active/button:font-bold">
Nested groups
</Text>
</Pressable>
</Pressable>
対応バリアント:group-active:*(押下)、group-focus:*(フォーカス)。名前付きバリアント:group-active/{name}:*、group-focus/{name}:*。
対応するグループ親:Pressable(押下+フォーカス)、Text(押下 — onPress が必要です。空の関数でも構いません)。TouchableOpacity、TouchableHighlight、TouchableWithoutFeedback、TextInput はグループ親として機能しません。group を付けた Pressable で囲んでください。
非対応:group-hover:*(ネイティブではポインターホバーがないため)、group-disabled:*(解析されますが、シャドウツリーのトリガーがありません)、任意の group-[.selector]:* バリアント、暗黙的な in-* バリアント。
デフォルトスタイル(Pro 1.2.0 以降、実験的)
デフォルトスタイルを使用すると、Pro ユーザーは組み込みの React Native コンポーネントのベースラインスタイルを CSS で直接定義できます。デフォルトでは無効になっており、実験的機能フラグが必要です。
// metro.config.js
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
experimental: {
defaultStyles: true,
},
});
/* global.css */
View {
border-color: var(--color-primary);
}
Text {
font-family: Inter;
font-size: 16px;
}
効果:より具体的なスタイルで上書きされない限り、すべての View に border-color: var(--color-primary) が、すべての Text に font-family: Inter と font-size: 16px が設定されます。
ルール:
- Uniwind Pro
1.2.0+でのみ利用可能です - デフォルトでは無効です。有効にするには
experimental.defaultStyles: trueを設定します - 実験的機能です。すべてのユースケースで動作するとは限らず、今後のリリースで変更される可能性があります
- セレクターには HTML タグではなく、React Native のコンポーネント名を使用してください
- ベースラインスタイルとして扱ってください。直接指定した
classNameのスタイルで上書きできます
対応するコンポーネントセレクター:ActivityIndicator、FlatList、Image、ImageBackground、InputAccessoryView、KeyboardAvoidingView、Modal、Pressable、RefreshControl、SafeAreaView、ScrollView、SectionList、Switch、Text、TextInput、TouchableHighlight、TouchableNativeFeedback、TouchableOpacity、TouchableWithoutFeedback、View、VirtualizedList。
Suspense のサポート
React の Suspense 境界内のコンポーネントは正しく処理されます。サブツリーが一時停止している間、Uniwind は C++ シャドウエントリーを維持するため、テーマの更新やランタイムの変更(ダークモード、画面の向きなど)が一時停止中のノードにも届きます。ツリーが再開したときにはスタイルがすでに最新の状態になっているため、古いテーマが一瞬表示されることはありません。
ネイティブのインセット
SafeAreaListener の設定を削除します。インセットはネイティブレイヤーから注入されます。
// With Pro — just use safe area classes directly
<View className="pt-safe pb-safe">{/* content */}</View>
テーマのトランジション(Pro)
テーマ切り替え時にネイティブのアニメーション付きトランジションを適用します。iOS、Android、Web でサポートされています。
import { Uniwind, ThemeTransitionPreset } from 'uniwind';
// Fade transition
Uniwind.setTheme('dark', { preset: ThemeTransitionPreset.Fade });
// Slide transitions
Uniwind.setTheme('dark', { preset: ThemeTransitionPreset.SlideRightToLeft });
Uniwind.setTheme('light', { preset: ThemeTransitionPreset.SlideLeftToRight });
// Circle mask transitions (expand from a corner or center)
Uniwind.setTheme('ocean', { preset: ThemeTransitionPreset.CircleCenter });
// Blur transitions
Uniwind.setTheme('dark', { preset: ThemeTransitionPreset.Blur });
Uniwind.setTheme('dark', { preset: ThemeTransitionPreset.BlurRightToLeft });
// No animation
Uniwind.setTheme('light');
利用可能なプリセット:
| プリセット | 効果 |
|---|---|
ThemeTransitionPreset.None | アニメーションなしで即座に切り替え |
ThemeTransitionPreset.Fade | テーマ間をクロスフェード |
ThemeTransitionPreset.SlideRightToLeft | 新しいテーマが右からスライドイン |
ThemeTransitionPreset.SlideLeftToRight | 新しいテーマが左からスライドイン |
ThemeTransitionPreset.CircleTopRight | 右上から円形マスクが拡大 |
ThemeTransitionPreset.CircleTopLeft | 左上から円形マスクが拡大 |
ThemeTransitionPreset.CircleBottomRight | 右下から円形マスクが拡大 |
ThemeTransitionPreset.CircleBottomLeft | 左下から円形マスクが拡大 |
ThemeTransitionPreset.CircleCenter | 中央から円形マスクが拡大 |
ThemeTransitionPreset.Blur | ぼかしながら消えるアニメーション |
ThemeTransitionPreset.BlurRightToLeft | 右から左への方向性のあるぼかし |
ThemeTransitionPreset.BlurLeftToRight | 左から右への方向性のあるぼかし |
セットアップの診断
スタイルが機能しない場合は、次の順に確認してください。
1. package.json
- 依存関係に
"uniwind"(または"uniwind-pro")がある "tailwindcss"が v4 以上(^4.0.0)- Pro の場合:
react-native-nitro-modules、react-native-reanimated、react-native-worklets
2. metro.config.js
'uniwind/metro'からwithUniwindConfigをインポートしているwithUniwindConfigが最も外側のラッパーになっているcssEntryFileが相対パスの文字列(例:'./global.css')になっているpath.resolve()や絶対パスを使用していない- Pro のデフォルトスタイルを使う場合:
experimental.defaultStyles: trueが設定されている
3. global.css
@import 'tailwindcss';と@import 'uniwind';の両方が含まれているApp.tsxまたはルートレイアウトでインポートされている。index.ts/index.jsではインポートしない- 配置場所によって、Tailwind がスキャンするアプリのルートが決まる
4. babel.config.js(Pro のみ)
- プラグイン配列に
'react-native-worklets/plugin'がある
5. TypeScript
uniwind-types.d.tsが存在する(Metro の実行後に生成)tsconfig.jsonに含まれている、またはsrc//app/ディレクトリに配置されている
6. ビルド
- 設定変更後に Metro サーバーを再起動している
- Metro のキャッシュをクリアしている(
npx expo start --clearまたはnpx react-native start --reset-cache) - ネイティブの再ビルドを実施している(Pro の場合、または依存関係の変更後)
トラブルシューティング
| 症状 | 原因 | 対処方法 |
|---|---|---|
| スタイルが適用されない | global.css にインポートがない | @import 'tailwindcss'; @import 'uniwind'; を追加します |
| スタイルが適用されない | global.css を index.js でインポートしている | インポートを App.tsx または _layout.tsx に移動します |
| クラスが検出されない | global.css がネストされたディレクトリにあり、コンポーネントが別の場所にある | global.css に @source '../components' を追加します |
| className で TypeScript エラーが発生する | 型定義ファイルがない | Metro を実行して uniwind-types.d.ts を生成します |
withUniwindConfig is not a function | インポートが正しくない | require('uniwind') ではなく require('uniwind/metro') を使用します |
| ホットリロードでアプリ全体が再読み込みされる | global.css を間違ったファイルでインポートしている | App.tsx またはルートレイアウトに移動します |
cssEntryFile エラー/Metro がクラッシュする | 絶対パスを使用している | 相対パス './global.css' を使用します |
withUniwindConfig が最も外側にない | 別のラッパーが Uniwind をラップしている | 順序を入れ替えて、Uniwind を最も外側にします |
| ダークテーマが機能しない | @variant dark がない | @layer theme でダークバリアントを定義します |
| カスタムテーマが表示されない | metro config に登録されていない | extraThemes 配列に追加して、Metro を再起動します |
| フォントが読み込まれない | フォント名が一致していない | CSS のフォント名はファイル名と完全に一致させます(拡張子なし) |
rem の値が大きすぎる/小さすぎる | 基準となる rem が正しくない | NativeWind との互換性のために polyfills: { rem: 14 } を設定します |
| 未対応 CSS の警告が表示される | Web 固有の CSS を使用している | debug: true を有効にして特定し、未対応のプロパティを削除します |
Failed to serialize javascript object | 複雑な CSS、循環参照、または古いキャッシュ | キャッシュをクリアします:watchman watch-del-all; rm -rf node_modules/.cache; npx expo start --clear。また、CSS クラスを含む docs/markdown ファイルがスキャン対象のパスにないか確認してください(下記参照) |
llms-full.txt または docs で Failed to serialize javascript object が発生する | プロジェクトディレクトリ内の CSS クラスを含む docs/markdown ファイルが Tailwind によってスキャンされている | CSS の例を含む .md ファイルをプロジェクトルートの外に移動するか、.gitignore に追加して Tailwind のスキャナーがスキップするようにします |
unstable_enablePackageExports の競合 | アプリで package exports が無効になっている | Uniwind と culori に選択的リゾルバーを使用します |
| モノレポのパッケージのクラスが見つからない | Tailwind のスキャン対象に含まれていない | global.css に @source '../../packages/ui' を追加します |
本番環境で node_modules のライブラリのクラスが見つからない(bun) | Bun はシンボリックリンクを使用するため、Tailwind の Oxide スキャナーがたどれない | 解決済みパス @source "../../node_modules/heroui-native/lib" を使用し、.npmrc に public-hoist-pattern[]=heroui-native を追加します |
withUniwind で active: が機能しない | withUniwind はインタラクティブな状態セレクターをサポートしていません | RN コアの Pressable/TextInput/Switch のみが active:/focus:/disabled: をサポートしています。withUniwind でラップしたサードパーティ製のプレス可能コンポーネントでは状態が適用されません |
withUniwind のカスタムマッピングで className と style のマージが上書きされる | 手動マッピングを指定すると、style プロパティがマージされない | className と style をマージするには、自動マッピング(第 2 引数なし)を使用します。手動マッピングと className を併用するには、二重にラップします:withUniwind(withUniwind(Comp), { mapping }) |
withUniwind が ref のジェネリック型(例:FlashList<T>)を失う | HOC における TypeScript の制限 | ref を手動でキャストします:ref={scrollRef as any} |
プラットフォーム固有のフォント:@theme ブロックエラー | @theme {} 内に @media ios/android がある | 代わりに @layer theme { :root { @variant ios { ... } } } を使用します — @theme が受け付けるのはカスタムプロパティのみで、プラットフォームの選択には @media ではなく @variant を使用します |
Android(RN 0.82 以降)で Uniwind.setTheme('system') がクラッシュする | RN 0.82 で Appearance API が変更された | 最新の Uniwind に更新します(修正済み)。古い Uniwind と RN 0.82 以降の組み合わせでは setTheme('system') を使用しないでください |
| 初回読み込み時にスタイルが一瞬表示された後、消える(Android) | コンポーネントのリスナーがマウントされる前に SafeAreaListener が呼び出される | 最近のバージョンで修正済みです。問題が続く場合は、Uniwind が最新であることを確認してください |
useTVEventHandler が未定義 | Uniwind のモジュール置換が tvOS のエクスポートに干渉している | v1.2.1 以降で修正済みです。Uniwind を更新してください |
Web で @layer theme の変数がレンダリングされない | RNW と Expo SDK 55 の不具合 | v1.4.1 以降で修正済みです。Uniwind を更新してください |
アプリ起動時に updateCSSVariables が間違ったテーマを適用する | 複数のテーマに対して連続して呼び出しており、初回レンダリングでは最後の呼び出しが適用される | 現在のテーマに対する updateCSSVariables の呼び出しを最後にします。初回読み込み後は順序は関係ありません |
| Pro:アニメーションが機能しない | Babel プラグインがない | babel.config.js に react-native-worklets/plugin を追加します |
| Pro:モジュールが見つからない | ネイティブの再ビルドをしていない | npx expo prebuild --clean の後に npx expo run:ios を実行します |
| Pro:postinstall に失敗した | パッケージマネージャーがスクリプトをブロックしている | trustedDependencies(bun)に追加するか、yarn/pnpm を設定します |
| Pro:認証の有効期限が切れた | ログインセッションの有効期限が切れた(有効期間は 180 日) | npx uniwind-pro を実行して、再ログインします |
| Pro:ダウンロード制限に達した | 月間ダウンロード制限に達した | Pro ダッシュボードを確認してください。制限は毎月リセットされます |
Pro:Uniwind.updateInsets が不要に呼び出される | Pro はインセットをネイティブに挿入する | Pro では Uniwind.updateInsets は何もしません。Pro を使用する場合は SafeAreaListener の設定を削除してください |
| Pro:テーマの切り替え時にクラッシュする | ThemeTransitionPreset のインポートがないか、アプリの準備完了前に呼び出している | 'uniwind' からインポートします。トランジション付きで setTheme を呼び出す前に、アプリが完全にマウントされていることを確認してください |
| Pro:コンポーネントのデフォルトスタイルが適用されない | 機能が無効か、セレクターがサポートされていない | Uniwind Pro 1.2.0 以降を使用し、experimental.defaultStyles: true を有効にして Metro を再起動し、View や Text などのサポート対象の RN コンポーネントセレクターを使用します |
unstable_enablePackageExports 選択的リゾルバー
アプリで unstable_enablePackageExports を無効にしている場合(暗号資産アプリでよくあります)、選択的リゾルバーを使用してください。
config.resolver.unstable_enablePackageExports = false;
config.resolver.resolveRequest = (context, moduleName, platform) => {
if (['uniwind', 'culori'].some((prefix) => moduleName.startsWith(prefix))) {
return context.resolveRequest(
{ ...context, unstable_enablePackageExports: true },
moduleName,
platform
);
}
return context.resolveRequest(context, moduleName, platform);
};
FAQ
Expo Router では global.css をどこに配置すればよいですか?
プロジェクトのルートです。app/_layout.tsx でインポートしてください。app/ 内に配置する場合は、兄弟ディレクトリ用の @source を追加してください。
Uniwind は Expo Go で動作しますか? Free:はい。Pro:いいえ。ネイティブの再ビルド(開発ビルド)が必要です。
tailwind.config.js を使用できますか?
いいえ。Uniwind は Tailwind v4 を使用しており、設定はすべて global.css 内の @theme で行います。
JavaScript から CSS 変数にアクセスするにはどうすればよいですか?
コンポーネント内では useCSSVariable('--color-primary')(リアクティブ)を使用します。React の外では Uniwind.getCSSVariable('--color-primary')(一度だけ取得、1.6.4+)を使用します。クラス名で使用しない変数は、@theme static で定義してください。
Platform.select() を使用できますか?
はい。ただし、プラットフォームセレクター(ios:pt-12 android:pt-6)を推奨します。よりシンプルで、インポートも不要です。
Next.js はサポートされていますか?
公式にはサポートされていません。コミュニティプラグイン:uniwind-plugin-next。Next.js では、標準の Tailwind CSS を使用してください。
Vite はサポートされていますか?
はい。v1.2.0 以降でサポートされています。@tailwindcss/vite と併せて uniwind/vite プラグインを使用してください。
CSS の変更時にアプリ全体がリロードされるのはなぜですか?
Metro は、多数のプロバイダーを含むファイルをホットリロードできません。コンポーネントツリーのより深い位置に global.css のインポートを移動してください。
スタイルの優先順位はどうなっていますか?
bg-red-500! のような重要ユーティリティは、同じプロパティに対する重要でないユーティリティを上書きし、バリアント(active:bg-red-500!、ios:pt-12!)でも使用できます。インラインの style は、重要ユーティリティであっても常に className を上書きします。静的なスタイルには className を使用し、インラインスタイルは本当に動的な値にのみ使用してください。クラス名が競合する可能性のあるコンポーネントライブラリでは、tailwind-merge の cn() を使用してください。
カスタムフォントを読み込むにはどうすればよいですか?
フォントファイルを読み込み(Expo:app.json の expo-font プラグイン、Bare RN:react-native-asset)、CSS でマッピングします:@theme { --font-sans: 'Roboto-Regular'; }。フォント名はファイル名と完全に一致している必要があります。上記の Fonts セクションを参照してください。
プロパティの値に基づいてスタイルを設定するにはどうすればよいですか?
データセレクターを使用します:data-[selected=true]:ring-2。等価チェックのみサポートされています。上記の Data Selectors セクションを参照してください。
グラデーションを使用するにはどうすればよいですか?
組み込み機能:bg-gradient-to-r from-red-500 to-green-500。角度指定(bg-linear-90)と任意の値(bg-linear-[45deg,#f00_0%,#00f_100%])にも対応しています。上記の Gradients セクションを参照してください。
className の重複排除はどのように機能しますか?
Uniwind は、競合する className を自動では重複排除しません。cn() ユーティリティと tailwind-merge を使用してください。上記の cn Utility セクションを参照してください。
「Failed to serialize javascript object」のデバッグ方法を教えてください。
キャッシュをクリアします:watchman watch-del-all; rm -rf node_modules/.cache; npx expo start --clear。Metro の設定で debug: true を有効にし、問題のある CSS パターンを特定してください。上記の Troubleshooting テーブルを参照してください。
セーフエリアの className を有効にするにはどうすればよいですか?
Free:react-native-safe-area-context をインストールし、ルートを SafeAreaListener でラップして、Uniwind.updateInsets(insets) を呼び出します。Pro:自動で有効になるため、設定は不要です。その後、pt-safe、pb-safe などを使用してください。上記の Safe Area Utilities セクションを参照してください。
Uniwind と相性のよい UI キットはどれですか?
React Native Reusables(shadcn の考え方に基づく、コピー&ペースト可能なコンポーネント)と HeroUI Native(Uniwind 向けに最適化された完全なライブラリ)です。どのライブラリも withUniwind ラッパーを使って動作します。上記の UI Kit Compatibility セクションを参照してください。
単一のコンポーネントにテーマを適用できますか?
はい。ScopedTheme を使用します:<ScopedTheme theme="dark"><Card /></ScopedTheme>。グローバルテーマを変更せずに、サブツリー内でテーマを強制的に適用します。Theming セクションを参照してください。
active: は react-native-gesture-handler の Pressable で動作しますか?
いいえ。withUniwind はインタラクティブな状態セレクター(active:、focus:、disabled:)をサポートしていません。これらをサポートしているのは、コア RN の Pressable、TextInput、Switch のみです。RNGH コンポーネントでは、状態と onPressIn/onPressOut を使用してください。
デフォルトの border の色はカスタマイズできますか?
はい。@utility border を使ってクラス全体を上書きします。
@utility border {
border-width: 1px;
border-style: solid;
border-color: var(--color-primary);
}
これにより組み込みの border の動作が完全に置き換えられるため、引き続き必要なプロパティは再宣言してください。または、border border-gray-300 を明示的に使うか、@theme で --color-border を定義して border-border を使います。
@theme {} でプラットフォーム固有のフォントを使えますか?
いいえ。@theme {} が受け付けるのはカスタムプロパティのみです。代わりに @layer theme { :root { @variant ios { --font-sans: '...'; } } } を使います。注:CSS でプラットフォームを選択するには、@media ではなく @variant を使ってください。
Uniwind は ESM の metro 設定(metro.config.mjs または metro.config.ts)で動作しますか?
はい。v1.3.0 以降で動作します。Class extends value is not a constructor が発生した場合は、Uniwind を更新するか、CommonJS の metro.config.js を使ってください。
node_modules のクラスが開発環境では動作するのに、本番環境(bun)では動作しないのはなぜですか?
Bun はパッケージをシンボリックリンクします。Tailwind の Oxide スキャナーはそれをたどれません。解決済みの @source パスを使い、パッケージをホイストしてください。詳細は UI Kit Compatibility を参照してください。
デバッグ時にすべてのキャッシュをクリアするにはどうすればよいですか?
watchman watch-del-all 2>/dev/null; rm -rf node_modules/.cache && npx expo start --clear を実行します。これにより、Watchman、Babel/バンドラーのキャッシュ、Expo 内部のキャッシュがクリアされます。
MCP サーバー
Uniwind は、AI を活用した開発向けの公式 MCP サーバーを提供しています。ユーザーの環境が MCP に対応している場合は、セットアップをおすすめしてください:https://docs.uniwind.dev/mcp
関連スキル
NativeWind → Uniwind の移行:NativeWind から移行するには、migrate-nativewind-to-uniwind スキルを使います。パッケージの削除、設定の移行、Tailwind v4 へのアップグレード、cssInterop の削除、テーマの変換、すべての破壊的変更に対応しています。テスト済みで、本番環境で使用できます。
重要:Uniwind の API を推測しないでください。Uniwind の API、フック、コンポーネント、設定オプションに不確かな点がある場合は、公式ドキュメントを取得して確認してください:https://docs.uniwind.dev/llms-full.txt