uniwind
React NativeプロジェクトでTailwind CSS v4を使用したスタイリングを行うためのスキルです。`className`によるコンポーネントのスタイリング、`withUniwindConfig`や`metro.config.js`の設定、ダーク/ライトテーマや`ios:`/`android:`などのプラットフォームセレクター、レスポンシブブレークポイント、CSS変数、`tv()`バリアント、アニメーション・グラデーション・セーフエリアなどのUniwind Pro機能、さらに`HeroUI Native`や`Gluestack`との連携まで幅広く対応します。なお、NativeWindからのマイグレーション作業は対象外です(`migrate-nativewind-to-uniwind`スキルを使用してください)。
description の原文を見る
> Uniwind — Tailwind CSS v4 styling for React Native. Use when adding, building, or styling components in a React Native project that uses Tailwind with className. Triggers on: className on RN components, Tailwind classes in RN, global.css with @import 'uniwind', withUniwindConfig, withUniwind, metro.config.js with Uniwind, useResolveClassNames, useCSSVariable, getCSSVariable, useUniwind, dark:/light: theming, platform selectors (ios:/android:/native:/web:/tv:), data-[prop=value], responsive breakpoints (sm:/md:/lg:), important utilities (!bg-red-500), tailwind-variants, tv() variants, ScopedTheme, Uniwind.setTheme, Uniwind.updateCSSVariables, @theme, @utility, @variant, CSS variables in RN, colorClassName, tintColorClassName, contentContainerClassName, Uniwind Pro (animations, transitions, shadow tree, native insets), safe area utilities, gradients, hairlineWidth(), fontScale(), pixelRatio(), light-dark(), OKLCH, cn, tailwind-merge, HeroUI Native, react-native-reusables, Gluestack. Does NOT handle migration — use migrate-nativewind-to-uniwind skill.
SKILL.md 本文
Uniwind — 完全リファレンス
Uniwind 1.6.0+ / Tailwind CSS v4 / React Native 0.81+ / Expo SDK 54+
ユーザーが低いバージョンを使用している場合は、最高の体験のために 1.6.0+ への更新をお勧めします。
Uniwind は React Native に Tailwind CSS v4 をもたらします。すべてのコア React Native コンポーネントは、既に className プロップをサポートしています。スタイルはビルド時にコンパイルされます。実行時のオーバーヘッドはありません。
重要なルール
- Tailwind v4 のみ —
@tailwind baseではなく@import 'tailwindcss'を使用します。Tailwind v3 はサポートされていません。 - クラス名を動的に構築しない — Tailwind はビルド時にスキャンします。
bg-${color}-500は動作しません。完全な文字列リテラル、マッピングオブジェクト、または三項演算子を使用してください。 cssInteropまたはremapPropsを使用しない — これらは NativeWind API です。Uniwind はグローバルコンポーネントをオーバーライドしません。tailwind.config.jsがない — すべての設定はglobal.cssの@themeと@layer themeを経由します。- 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-プリフィックスが REQUIRED — これは非常に重要です。color(Button、ActivityIndicator)、tintColor(Image)、thumbColor(Switch)、placeholderTextColor(TextInput) などのプロップはstyleオブジェクトの一部ではありません。対応する{propName}ClassNameプロップをaccent-でプリフィックスされたクラスと一緒に使用する必要があります。例:<ActivityIndicator colorClassName="accent-blue-500" />ではなく<ActivityIndicator className="text-blue-500" />。通常の Tailwind カラークラス (text-blue-500など) はclassNameでのみ機能します (styleにマッピング)。非スタイルカラープロップの場合は、常にaccent-を使用してください。 - rem デフォルトは 16px — NativeWind は 14px を使用していました。マイグレーション時に metro 設定で
polyfills: { rem: 14 }を設定します。 cssEntryFileは相対パス文字列である必要があります —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。Important ユーティリティは同じスタイルプロパティの non-important ユーティリティをオーバーライドしますが、インラインstyleは依然として className をオーバーライドします。
セットアップ
インストール
# または他のパッケージマネージャー
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 または App.tsx
import './global.css';
global.css を含むディレクトリはアプリのルートです — Tailwind はこのディレクトリから始まるクラス名をスキャンします。
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 は最も外側のラッパーである必要があります
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css', // 必須 — プロジェクトルートからの相対パス
polyfills: { rem: 16 }, // オプション — 基本 rem 値 (デフォルト 16)
extraThemes: ['ocean', 'sunset'], // オプション — light/dark を超えたカスタムテーマ
dtsFile: './uniwind-types.d.ts', // オプション — TypeScript 型出力パス
debug: true, // オプション — 開発時にサポートされていない CSS をログ出力
isTV: false, // オプション — TV プラットフォームサポートを有効化
});
ほとんどのフローではデフォルトを保持し、cssEntryFile のみを指定します。
ラッパーの順序 — Uniwind はすべてをラップする必要があります:
// 正解
module.exports = withUniwindConfig(withOtherConfig(config, opts), { cssEntryFile: './global.css' });
// 誤り — Uniwind は最も外側ではありません
module.exports = withOtherConfig(withUniwindConfig(config, { cssEntryFile: './global.css' }), opts);
Vite 設定 (v1.2.0+)
ユーザーが storybook セットアップを持っている場合、追加の vite 設定を追加します:
import tailwindcss from '@tailwindcss/vite';
import { uniwind } from 'uniwind/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
tailwindcss(),
uniwind({
cssEntryFile: './src/global.css',
dtsFile: './src/uniwind-types.d.ts',
}),
],
});
TypeScript
Uniwind は Metro を実行後に .d.ts ファイル (デフォルト: ./uniwind-types.d.ts) を自動生成します。src/ または app/ に配置すると自動的に含まれるか、tsconfig.json に追加します:
{ "include": ["./uniwind-types.d.ts"] }
ユーザーが className に関する TypeScript エラーを持つ場合は、Metro サーバーを実行して d.ts ファイルを生成するだけです。
Expo Router プレースメント
project/
├── app/_layout.tsx ← ここで '../global.css' をインポート
├── components/
├── global.css ← プロジェクトルート (最適な場所)
└── 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"]
}
モノレポサポート
global.css で CSS エントリファイルのディレクトリ外のパッケージに対して @source ディレクティブを追加します:
@import 'tailwindcss';
@import 'uniwind';
@source "../../packages/ui/src";
@source "../../packages/shared/src";
Uniwind クラスを含む node_modules パッケージ (共有 UI ライブラリなど) にも必要です。
コンポーネントバインディング
すべてのコア React Native コンポーネントは、既に className をサポートしています。サブスタイル用の追加 className プロップ (contentContainerClassName など) と非スタイルカラープロップ (accent- プリフィックスが必須) を持つものもあります。
完全なリファレンス
凡例: ⚡ でマークされたプロップは accent- プリフィックスを必要とします。括弧内のプロップはプラットフォーム固有です。
View
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
Text
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
selectionColorClassName | selectionColor | ⚡ accent- |
Pressable
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
active:、disabled:、focus: 状態セレクタをサポートします。
Image
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
tintColorClassName | tintColor | ⚡ accent- |
TextInput
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
cursorColorClassName | cursorColor | ⚡ accent- |
selectionColorClassName | selectionColor | ⚡ accent- |
placeholderTextColorClassName | placeholderTextColor | ⚡ accent- |
selectionHandleColorClassName | selectionHandleColor | ⚡ accent- |
underlineColorAndroidClassName | underlineColorAndroid (Android) | ⚡ accent- |
focus:、active:、disabled: 状態セレクタをサポートします。
ScrollView
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
FlatList
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
columnWrapperClassName | columnWrapperStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
SectionList
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
VirtualizedList
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
ListHeaderComponentClassName | ListHeaderComponentStyle | — |
ListFooterComponentClassName | ListFooterComponentStyle | — |
endFillColorClassName | endFillColor | ⚡ accent- |
Switch
| プロップ | マップ先 | プリフィックス |
|---|---|---|
thumbColorClassName | thumbColor | ⚡ accent- |
trackColorOnClassName | trackColor.true (on) | ⚡ accent- |
trackColorOffClassName | trackColor.false (off) | ⚡ accent- |
ios_backgroundColorClassName | ios_backgroundColor (iOS) | ⚡ accent- |
注: Switch は className をサポートしません (className?: never in types)。上記のカラー固有の className プロップのみを使用します。disabled: 状態セレクタをサポートします。
ActivityIndicator
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
colorClassName | color | ⚡ accent- |
Button
| プロップ | マップ先 | プリフィックス |
|---|---|---|
colorClassName | color | ⚡ accent- |
注: Button は className をサポートしません (RN Button に style プロップがない)。
Modal
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
backdropColorClassName | backdropColor | ⚡ accent- |
RefreshControl
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
colorsClassName | colors (Android) | ⚡ accent- |
tintColorClassName | tintColor (iOS) | ⚡ accent- |
titleColorClassName | titleColor (iOS) | ⚡ accent- |
progressBackgroundColorClassName | progressBackgroundColor (Android) | ⚡ accent- |
ImageBackground
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
imageClassName | imageStyle | — |
tintColorClassName | tintColor | ⚡ accent- |
SafeAreaView
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
KeyboardAvoidingView
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
contentContainerClassName | contentContainerStyle | — |
InputAccessoryView
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
backgroundColorClassName | backgroundColor | ⚡ accent- |
TouchableHighlight
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
underlayColorClassName | underlayColor | ⚡ accent- |
active:、disabled: 状態セレクタをサポートします。
TouchableOpacity
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
active:、disabled: 状態セレクタをサポートします。
TouchableNativeFeedback
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
active:、disabled: 状態セレクタをサポートします。
TouchableWithoutFeedback
| プロップ | マップ先 | プリフィックス |
|---|---|---|
className | style | — |
active:、disabled: 状態セレクタをサポートします。
使用例
import { View, Text, Pressable, TextInput, ScrollView, FlatList, Switch, Image, ActivityIndicator, Modal, RefreshControl, Button } from 'react-native';
// View — 基本的なレイアウト
<View className="flex-1 bg-background p-4">
<Text className="text-foreground text-lg font-bold">Title</Text>
</View>
// Pressable — press/focus 状態付き
<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 — focus 状態と accent- カラープロップ付き
<TextInput
className="border border-border rounded-lg px-4 py-2 text-base text-foreground focus:border-primary"
placeholderTextColorClassName="accent-muted"
selectionColorClassName="accent-primary"
cursorColorClassName="accent-primary"
selectionHandleColorClassName="accent-primary"
underlineColorAndroidClassName="accent-transparent"
placeholder="Enter text..."
/>
// ScrollView — コンテンツコンテナ付き
<ScrollView className="flex-1" contentContainerClassName="p-4 gap-4">
{/* content */}
</ScrollView>
// FlatList — すべてのサブスタイルプロップ付き
<FlatList
className="flex-1"
contentContainerClassName="p-4 gap-3"
columnWrapperClassName="gap-3"
ListHeaderComponentClassName="pb-4"
ListFooterComponentClassName="pt-4"
endFillColorClassName="accent-gray-100"
numColumns={2}
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
/>
// Switch — className サポートなし、カラー固有のプロップのみを使用
<Switch
thumbColorClassName="accent-white"
trackColorOnClassName="accent-primary"
trackColorOffClassName="accent-gray-300 dark:accent-gray-700"
ios_backgroundColorClassName="accent-gray-200"
/>
// Image — tint color
<Image className="w-6 h-6" tintColorClassName="accent-primary" source={icon} />
// ActivityIndicator
<ActivityIndicator className="m-4" colorClassName="accent-primary" size="large" />
// Button — colorClassName のみ (className なし)
<Button colorClassName="accent-primary" title="Submit" onPress={handleSubmit} />
// Modal — backdrop color
<Modal className="flex-1" backdropColorClassName="accent-black/50">
{/* content */}
</Modal>
// RefreshControl — プラットフォーム固有のカラープロップ
<RefreshControl
className="p-4"
tintColorClassName="accent-primary"
titleColorClassName="accent-gray-500"
colorsClassName="accent-primary"
progressBackgroundColorClassName="accent-white dark:accent-gray-800"
/>
// ImageBackground — 個別の画像スタイリング
<ImageBackground
className="flex-1 justify-center items-center"
imageClassName="opacity-50"
tintColorClassName="accent-blue-500"
source={bgImage}
>
<Text className="text-white text-2xl font-bold">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 コンポーネントは color、tintColor、thumbColor などのプロップを持っており、これらは style オブジェクトの一部ではありません。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) があります。これらは別の {propName}ClassName プロップが必要で、accent- プリフィックス付きです。accent- がないと、クラスはスタイルオブジェクトに解決されます — しかし、これらのプロップは通常の色文字列を期待します。
// 誤り — className は style を設定しますが、ActivityIndicator の color は style プロップではありません
<ActivityIndicator className="text-blue-500" /> // color は設定されません
// 正解 — 専用の colorClassName プロップを accent- プリフィックスで使用
<ActivityIndicator colorClassName="accent-blue-500" /> // color は #3b82f6 に設定されます
// 誤り — tintColor は Image の style プロップではありません
<Image className="tint-blue-500" source={icon} /> // 動作しません
// 正解
<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';
// モジュールレベルのラップ (レンダー関数内で絶対に実行しない)
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);
// その後、すべてのところでインポート:
import { Image } from '@/components/styled';
複数のファイルで同じコンポーネントに対して withUniwind を呼び出さない でください。
重要: react-native または react-native-reanimated からのコンポーネントで withUniwind を使用しないでください。これらは既にビルトイン className サポートを備えています:
// 誤り — View はネイティブで既に className をサポートしています
const StyledView = withUniwind(View); // これをしないでください
const StyledText = withUniwind(Text); // これをしないでください
const StyledAnimatedView = withUniwind(Animated.View); // これをしないでください
// 正解 — サードパーティコンポーネントのみをラップします
const StyledExpoImage = withUniwind(ExpoImage); // expo-image
const StyledBlurView = withUniwind(BlurView); // expo-blur
const StyledMotiView = withUniwind(MotiView); // moti
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 スクリーンオプション
<Stack.Navigator screenOptions={{ headerStyle, cardStyle }} />
比較
| 機能 | withUniwind | useResolveClassNames |
|---|---|---|
| セットアップ | コンポーネントあたり 1 回 | 使用ごと |
| パフォーマンス | 最適化 | 少し遅い |
| 最適な用途 | 再利用可能なコンポーネント | ワンオフ、ナビゲーション設定 |
| 構文 | className="..." | style={...} |
動的クラス名
これをしない (Tailwind はビルド時にスキャンします)
// 壊れている — 変数を含むテンプレートリテラル
<View className={`bg-${color}-500`} />
<Text className={`text-${size}`} />
正しいパターン
// 完全なクラス名を持つ三項演算子
<View className={isActive ? 'bg-primary' : 'bg-muted'} />
// マッピングオブジェクト
const colorMap = {
primary: 'bg-blue-500 text-white',
danger: 'bg-red-500 text-white',
ghost: 'bg-transparent text-foreground',
};
<Pressable className={colorMap[variant]} />
// 複数の条件のための配列 join
<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 は、競合するクラス名を自動的に重複排除しません。これは、同じプロパティが複数のクラスに表示される場合、両方が適用され、結果は予測不可能 という意味です。これは特に、カスタム 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;
}
// 誤り — .card (padding: 16px) と p-6 (padding: 24px) の両方が適用され、結果は予測不可能です
<View className="card p-6" />
// 正解 — cn は重複排除し、p-6 が .card のパディングを上書きします
<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 がボックスシャドウのみを設定し、Tailwind クラスも設定しない場合)
Important ユーティリティとスタイル特異性
Uniwind は Tailwind の important モディファイア (!) をサポートし、同じスタイルプロパティの別のユーティリティをオーバーライドする必要があるユーティリティ用です。
import { View, Pressable } from 'react-native';
// !bg-red-500 は bg-blue-500 より高い優先度を持ちます
<View className="bg-blue-500 !bg-red-500" />;
// Important ユーティリティは状態とプラットフォームバリアントで機能します
<Pressable className="bg-blue-500 active:!bg-red-500" />;
<View className="pt-4 ios:!pt-12 android:!pt-8" />;
優先度ルール:
- Important ユーティリティ (
!bg-red-500) は同じプロパティの non-important ユーティリティ (bg-blue-500) をオーバーライドします。 - Important バリアントは正常に機能します:
active:!bg-red-500、ios:!pt-12、dark:!text-white。 - インライン
styleは常に勝ちます、important className ユーティリティの場合でも:<View className="!bg-red-500" style={{ backgroundColor: 'blue' }} />は青でレンダリングされます。 !は控えめに使用してください。再利用可能なコンポーネントとコンシューマーオーバーライドの場合は、cn()withtailwind-mergeを優先してください。
テーマ
クイックセットアップ (dark: プリフィックス)
すぐに動作します — 設定は不要です:
<View className="bg-white dark:bg-gray-900">
<Text className="text-black dark:text-white">Themed</Text>
</View>
小規模なアプリとプロトタイピングに最適です。カスタムテーマには対応しません。
スケーラブルセットアップ (CSS 変数)
global.css で定義し、dark: プリフィックスなしですべての場所で使用します:
@layer theme {
:root {
@variant light {
--color-background: #ffffff;
--color-foreground: #111827;
--color-foreground-secondary: #6b7280;
--color-card: #ffffff;
--color-border: #e5e7eb;
--color-muted: #9ca3af;
--color-primary: #3b82f6;
--color-danger: #ef4444;
--color-success: #10b981;
}
@variant dark {
--color-background: #000000;
--color-foreground: #ffffff;
--color-foreground-secondary: #9ca3af;
--color-card: #1f2937;
--color-border: #374151;
--color-muted: #6b7280;
--color-primary: #3b82f6;
--color-danger: #ef4444;
--color-success: #10b981;
}
}
}
// 現在のテーマに自動的に適応 — dark: プリフィックスは不要です
<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;
/* 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';
// 命令型 (再レンダリングなし)
Uniwind.setTheme('dark'); // dark を強制
Uniwind.setTheme('light'); // light を強制
Uniwind.setTheme('system'); // デバイスに従う (適応型テーマを再有効化)
Uniwind.setTheme('ocean'); // カスタムテーマ (extraThemes に含まれる必要があります)
Uniwind.currentTheme; // 現在のテーマ名
Uniwind.hasAdaptiveThemes; // システムに従っている場合 true
// リアクティブフック (変更時に再レンダリング)
const { theme, hasAdaptiveThemes } = useUniwind();
Uniwind.setTheme('light') / setTheme('dark') は Appearance.setColorScheme を呼び出して、ネイティブコンポーネント (Alert、Modal、システムダイアログ) を同期します。
デフォルトでは Uniwind は「system」テーマを使用します — デバイスの色スキームに従います。ユーザーがそれをオーバーライドしたい場合は、希望のテーマで Uniwind.setTheme を呼び出すだけです。実行時のテーマ切り替えを避けるため、React コンポーネントの上で実行できます。
テーマスイッチャーの例
import { View, Pressable, Text, ScrollView } from 'react-native';
import { Uniwind, useUniwind } from 'uniwind';
export const ThemeSwitcher = () => {
const { theme, hasAdaptiveThemes } = useUniwind();
const activeTheme = hasAdaptiveThemes ? 'system' : theme;
const themes = [
{ name: 'light', label: '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 /> {/* dark テーマでレンダリング */}
</ScopedTheme>
<ScopedTheme theme="ocean">
<PreviewCard /> {/* ocean テーマでレンダリング */}
</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');
// 複数の変数を一度に
const [bg, fg] = useCSSVariable(['--color-background', '--color-foreground']) as [string, string]
使用: アニメーション、チャートライブラリ、サードパーティコンポーネント設定、デザイントークンを使った計算。
useCSSVariable の結果をキャストする必要があります。それは string | number | undefined を返す可能性があるためです。
Uniwind は与えられた変数が存在するか、どの型であるかを知らないため、ユニオン型を返します。
getCSSVariable
React の外側で CSS 変数値を読む (イベントハンドラー、非同期コールバック、ユーティリティモジュール、worklet)。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];
同じ値ルール as useCSSVariable (変数は className で使用されるか、@theme static で宣言される必要があります)。同じ戻り値型: string | number | undefined。必要に応じてキャストしてください。
リアクティブではありません — 値は一度読み込まれます。コンポーネント内のリアクティブ値の場合は useCSSVariable を使用してください。getCSSVariable をワンショット読み込みに使用 (onPress ハンドラー、ユーティリティ関数、ネイティブモジュール設定)。
実行時 CSS 変数更新
テーマ変数を実行時に更新 (例: ユーザーが選択したブランドカラーまたは API 駆動テーマ):
Uniwind.updateCSSVariables('light', {
'--color-primary': '#ff6600',
'--color-background': '#fafafa',
});
更新はテーマ固有で、即座に有効になります。
@theme static
className で使用されない JS 専用値の場合:
@theme static {
--chart-line-width: 2;
--chart-dot-radius: 4;
--animation-duration: 300;
}
useCSSVariable('--chart-line-width') でアクセス。用途: チャート設定、アニメーション期間、ネイティブモジュール値。
OKLCH カラーサポート
知覚的に均一な色フォーマット — より広いガマット、一貫性のある明るさ:
@layer theme {
:root {
@variant light {
--color-primary: oklch(0.5 0.2 240);
--color-background: oklch(1 0 0);
}
@variant dark {
--color-primary: oklch(0.6 0.2 240);
--color-background: oklch(0.13 0.004 17.69);
}
}
}
Display P3 カラーサポート
ワイドガマット色フォーマット (P3 色空間をサポートするデバイス向け — ほとんどの最新 iPhone および Mac)。Uniwind は color(display-p3 ...) 値をパースし、ネイティブ使用のために変換します:
@layer theme {
:root {
@variant light {
--color-primary: color(display-p3 0.2 0.4 1);
--color-accent: color(display-p3 1 0.3 0.3);
}
@variant dark {
--color-primary: color(display-p3 0.3 0.5 1);
--color-accent: color(display-p3 1 0.4 0.4);
}
}
}
プラットフォームセレクタ
className に直接、プラットフォーム固有のスタイルを適用:
// 個別プラットフォーム
<View className="ios:bg-red-500 android:bg-blue-500 web:bg-green-500" />
// native: ショートハンド (iOS + Android)
<View className="native:bg-blue-500 web:bg-gray-500" />
// TV プラットフォーム
<View className="tv:p-8 android-tv:bg-black apple-tv:bg-gray-900" />
// 他のユーティリティと組み合わせ
<View className="p-4 ios:pt-12 android:pt-6 web:pt-4" />
@layer theme のプラットフォームバリアント for global values (@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):
// ブール値プロップ
<Pressable
data-selected={isSelected}
className="border rounded px-3 py-2 data-[selected=true]:ring-2 data-[selected=true]:ring-primary"
/>
// 文字列プロップ
<View
data-state={isOpen ? 'open' : 'closed'}
className="p-4 data-[state=open]:bg-muted/50 data-[state=closed]:bg-transparent"
/>
// タブパターン
<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>
// トグルパターン
<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: — 押されたときに
<Pressable className="bg-primary active:bg-primary/80 active:opacity-90 active:scale-95">
<Text className="text-white">Press me</Text>
</Pressable>
// disabled: — disabled プロップが 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: — キーボード/アクセシビリティフォーカスのときに
<TextInput
className="border border-border rounded-lg px-4 py-2 focus:border-primary focus:ring-2 focus:ring-primary/20"
/>
<Pressable className="bg-card rounded-lg p-4 focus:ring-2 focus:ring-primary">
<Text className="text-foreground">Focusable</Text>
</Pressable>
状態サポートのあるコンポーネント:
- Pressable:
active:、disabled:、focus: - TextInput:
active:、disabled:、focus: - Switch:
disabled: - Text:
active:、disabled: - TouchableOpacity / TouchableHighlight / TouchableNativeFeedback / TouchableWithoutFeedback:
active:、disabled:
レスポンシブブレークポイント
モバイルファースト — プリフィックスなしのスタイルはすべてのサイズに適用、プリフィックス付きスタイルはそのブレークポイント以上で適用:
| プリフィックス | 最小幅 | 典型的デバイス |
|---|---|---|
| (なし) | 0px | すべて (モバイル) |
sm: | 640px | 大型スマートフォン |
md: | 768px | タブレット |
lg: | 1024px | ランドスケープタブレット |
xl: | 1280px | デスクトップ |
2xl: | 1536px | 大型デスクトップ |
// レスポンシブパディングとタイポグラフィ
<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>
// レスポンシブグリッド (1 列 → 2 列 → 3 列)
<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>
// レスポンシブ表示
<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
モバイルファースト設計 — ベーススタイルから始め (プリフィックスなし)、ブレークポイントで拡張:
// 正解 — モバイルファースト
<View className="w-full sm:w-3/4 md:w-1/2 lg:w-1/3" />
// 誤り — デスクトップファースト (逆順は混乱し、脆弱)
<View className="w-full lg:w-1/2 md:w-3/4 sm:w-full" />
セーフエリアユーティリティ
パディング
| クラス | 説明 |
|---|---|
p-safe | すべての辺 |
pt-safe / pb-safe / pl-safe / pr-safe | 個別の辺 |
px-safe / py-safe | 水平 / 垂直 |
マージン
| クラス | 説明 |
|---|---|
m-safe | すべての辺 |
mt-safe / mb-safe / ml-safe / mr-safe | 個別の辺 |
mx-safe / my-safe | 水平 / 垂直 |
ポジショニング
| クラス | 説明 |
|---|---|
inset-safe | すべての辺 |
top-safe / bottom-safe / left-safe / right-safe | 個別の辺 |
x-safe / `y |
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- uni-stack
- リポジトリ
- uni-stack/uniwind
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/uni-stack/uniwind / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。