expo-tailwind-setup について
Expo アプリで react-native-css と NativeWind v5 を使い、Tailwind CSS v4 を設定するためのスキルです。iOS、Android、Web 向けの設定に加え、CSS ラッパーコンポーネントや旧バージョンとの違いを説明します。
主な機能
- 01Expo で Tailwind CSS v4 を設定
- 02Metro と PostCSS の構成
- 03CSS ラッパーコンポーネントの作成
- 04JavaScript で CSS 変数を利用
- 05プラットフォーム別スタイルの適用
- 06スタイルと TypeScript の問題を調査
活用シーン
- 01新しい Expo アプリに共通スタイルを設定
- 02Tailwind v3 から設定を移行
- 03React Native コンポーネントに className 対応を追加
- 04適用されないスタイルを調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill expo-tailwind-setupインストール後、Claude Code で/expo-tailwind-setupを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yaml
詳細
react-native-css を使用した Expo 向け Tailwind CSS のセットアップ
使用する場面
react-native-css と NativeWind v5 を使用して、Expo に Tailwind CSS v4 をセットアップし、ユニバーサルなスタイリングを行う必要がある場合に、このスキルを使用してください。
このガイドでは、react-native-css と NativeWind v5 を使用して Expo に Tailwind CSS v4 をセットアップし、iOS、Android、Web 全体でユニバーサルなスタイリングを行う方法を説明します。
概要
このセットアップでは、次を使用します。
- Tailwind CSS v4 - モダンな CSS ファーストの設定
- react-native-css - React Native 用 CSS ランタイム
- NativeWind v5 - React Native で Tailwind を使うための Metro トランスフォーマー
- @tailwindcss/postcss - Tailwind v4 用 PostCSS プラグイン
これは、過去の NativeWind 5 プレビュー/ナイトリー版向けのレシピです。既存のアプリを変更する前に、現在の Expo SDK/React/NativeWind の互換性と、固定された依存関係の API を確認してください。Metro/Babel/PostCSS の設定を統合する際は、関係のないプラグインを削除しないでください。パッケージマネージャーによって解決方法は異なるため、Yarn の resolutions が npm にも適用されると考えないでください。ネイティブと Web のビルドに加え、アクセシビリティと視差効果の軽減の動作も検証してください。
インストール
# Install dependencies
npx expo install tailwindcss@^4 [email protected] [email protected] @tailwindcss/postcss tailwind-merge clsx
lightningcss との互換性を保つため、resolutions を追加します。
// package.json
{
"resolutions": {
"lightningcss": "1.30.1"
}
}
- lightningcss を使用するため、Expo では autoprefixer は不要です
- postcss はデフォルトで expo に含まれています
設定ファイル
Metro の設定
metro.config.js を作成または更新します。
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withNativewind } = require("nativewind/metro");
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
module.exports = withNativewind(config, {
// inline variables break PlatformColor in CSS variables
inlineVariables: false,
// We add className support manually
globalClassNamePolyfill: false,
});
PostCSS の設定
postcss.config.mjs を作成します。
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
グローバル CSS
src/global.css を作成します。
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";
/* Platform-specific font families */
@media android {
:root {
--font-mono: monospace;
--font-rounded: normal;
--font-serif: serif;
--font-sans: normal;
}
}
@media ios {
:root {
--font-mono: ui-monospace;
--font-serif: ui-serif;
--font-sans: system-ui;
--font-rounded: ui-rounded;
}
}
重要:Babel の設定は不要です
Tailwind v4 と NativeWind v5 では、Tailwind 用の babel.config.js は不要です。NativeWind の Babel プリセットが設定されている場合は削除してください。
// DELETE babel.config.js if it only contains NativeWind config
// The following is NO LONGER needed:
// module.exports = function (api) {
// api.cache(true);
// return {
// presets: [
// ["babel-preset-expo", { jsxImportSource: "nativewind" }],
// "nativewind/babel",
// ],
// };
// };
CSS コンポーネントラッパー
react-native-css では CSS 要素を明示的にラップする必要があるため、再利用可能なコンポーネントを作成します。
実際のアプリのエントリーポイント/ルートレイアウトで、src/global.css を一度だけインポートしてください。以下のバレルインポートを使用する場合は、src/tw/index.tsx に export { Image } from "./image"; を追加してください。
メインコンポーネント(src/tw/index.tsx)
import {
useCssElement,
useNativeVariable as useFunctionalVariable,
} from "react-native-css";
import { Link as RouterLink } from "expo-router";
import Animated from "react-native-reanimated";
import React from "react";
import {
View as RNView,
Text as RNText,
Pressable as RNPressable,
ScrollView as RNScrollView,
TouchableHighlight as RNTouchableHighlight,
TextInput as RNTextInput,
StyleSheet,
} from "react-native";
// CSS-enabled Link
export const Link = (
props: React.ComponentProps<typeof RouterLink> & { className?: string }
) => {
return useCssElement(RouterLink, props, { className: "style" });
};
Link.Trigger = RouterLink.Trigger;
Link.Menu = RouterLink.Menu;
Link.MenuAction = RouterLink.MenuAction;
Link.Preview = RouterLink.Preview;
// CSS Variable hook
export const useCSSVariable =
process.env.EXPO_OS !== "web"
? useFunctionalVariable
: (variable: string) => `var(${variable})`;
// View
export type ViewProps = React.ComponentProps<typeof RNView> & {
className?: string;
};
export const View = (props: ViewProps) => {
return useCssElement(RNView, props, { className: "style" });
};
View.displayName = "CSS(View)";
// Text
export const Text = (
props: React.ComponentProps<typeof RNText> & { className?: string }
) => {
return useCssElement(RNText, props, { className: "style" });
};
Text.displayName = "CSS(Text)";
// ScrollView
export const ScrollView = (
props: React.ComponentProps<typeof RNScrollView> & {
className?: string;
contentContainerClassName?: string;
}
) => {
return useCssElement(RNScrollView, props, {
className: "style",
contentContainerClassName: "contentContainerStyle",
});
};
ScrollView.displayName = "CSS(ScrollView)";
// Pressable
export const Pressable = (
props: React.ComponentProps<typeof RNPressable> & { className?: string }
) => {
return useCssElement(RNPressable, props, { className: "style" });
};
Pressable.displayName = "CSS(Pressable)";
// TextInput
export const TextInput = (
props: React.ComponentProps<typeof RNTextInput> & { className?: string }
) => {
return useCssElement(RNTextInput, props, { className: "style" });
};
TextInput.displayName = "CSS(TextInput)";
// AnimatedScrollView
export const AnimatedScrollView = (
props: React.ComponentProps<typeof Animated.ScrollView> & {
className?: string;
contentClassName?: string;
contentContainerClassName?: string;
}
) => {
return useCssElement(Animated.ScrollView, props, {
className: "style",
contentClassName: "contentContainerStyle",
contentContainerClassName: "contentContainerStyle",
});
};
// TouchableHighlight with underlayColor extraction
function XXTouchableHighlight(
props: React.ComponentProps<typeof RNTouchableHighlight>
) {
const { underlayColor, ...style } = StyleSheet.flatten(props.style) || {};
return (
<RNTouchableHighlight
underlayColor={underlayColor}
{...props}
style={style}
/>
);
}
export const TouchableHighlight = (
props: React.ComponentProps<typeof RNTouchableHighlight>
) => {
return useCssElement(XXTouchableHighlight, props, { className: "style" });
};
TouchableHighlight.displayName = "CSS(TouchableHighlight)";
Image コンポーネント(src/tw/image.tsx)
import { useCssElement } from "react-native-css";
import React from "react";
import { StyleSheet } from "react-native";
import Animated from "react-native-reanimated";
import { Image as RNImage } from "expo-image";
const AnimatedExpoImage = Animated.createAnimatedComponent(RNImage);
export type ImageProps = React.ComponentProps<typeof Image>;
function CSSImage(props: React.ComponentProps<typeof AnimatedExpoImage>) {
// @ts-expect-error: Remap objectFit style to contentFit property
const { objectFit, objectPosition, ...style } =
StyleSheet.flatten(props.style) || {};
return (
<AnimatedExpoImage
contentFit={objectFit}
contentPosition={objectPosition}
{...props}
source={
typeof props.source === "string" ? { uri: props.source } : props.source
}
// @ts-expect-error: Style is remapped above
style={style}
/>
);
}
export const Image = (
props: React.ComponentProps<typeof CSSImage> & { className?: string }
) => {
return useCssElement(CSSImage, props, { className: "style" });
};
Image.displayName = "CSS(Image)";
アニメーションコンポーネント(src/tw/animated.tsx)
import * as TW from "./index";
import RNAnimated from "react-native-reanimated";
export const Animated = {
...RNAnimated,
View: RNAnimated.createAnimatedComponent(TW.View),
};
使用方法
tw ディレクトリから、CSS でラップされたコンポーネントをインポートします。
import { View, Text, ScrollView, Image } from "@/tw";
export default function MyScreen() {
return (
<ScrollView className="flex-1 bg-white">
<View className="p-4 gap-4">
<Text className="text-xl font-bold text-gray-900">Hello Tailwind!</Text>
<Image
className="w-full h-48 rounded-lg object-cover"
source={{ uri: "https://example.com/image.jpg" }}
/>
</View>
</ScrollView>
);
}
カスタムテーマ変数
@theme を使用して、global.css にカスタムテーマ変数を追加します。
@layer theme {
@theme {
/* Custom fonts */
--font-rounded: "SF Pro Rounded", sans-serif;
/* Custom line heights */
--text-xs--line-height: calc(1em / 0.75);
--text-sm--line-height: calc(1.25em / 0.875);
--text-base--line-height: calc(1.5em / 1);
/* Custom leading scales */
--leading-tight: 1.25em;
--leading-snug: 1.375em;
--leading-normal: 1.5em;
}
}
プラットフォーム固有のスタイル
プラットフォーム固有のスタイルには、プラットフォームのメディアクエリを使用します。
@media ios {
:root {
--font-sans: system-ui;
--font-rounded: ui-rounded;
}
}
@media android {
:root {
--font-sans: normal;
--font-rounded: normal;
}
}
CSS 変数を使用した Apple システムカラー
Apple のセマンティックカラー用の CSS ファイルを作成します。
/* src/css/sf.css */
@layer base {
html {
color-scheme: light dark;
}
}
:root {
/* Accent colors with light/dark mode */
--sf-blue: light-dark(rgb(0 122 255), rgb(10 132 255));
--sf-green: light-dark(rgb(52 199 89), rgb(48 209 89));
--sf-red: light-dark(rgb(255 59 48), rgb(255 69 58));
/* Gray scales */
--sf-gray: light-dark(rgb(142 142 147), rgb(142 142 147));
--sf-gray-2: light-dark(rgb(174 174 178), rgb(99 99 102));
/* Text colors */
--sf-text: light-dark(rgb(0 0 0), rgb(255 255 255));
--sf-text-2: light-dark(rgb(60 60 67 / 0.6), rgb(235 235 245 / 0.6));
/* Background colors */
--sf-bg: light-dark(rgb(255 255 255), rgb(0 0 0));
--sf-bg-2: light-dark(rgb(242 242 247), rgb(28 28 30));
}
/* iOS native colors via platformColor */
@media ios {
:root {
--sf-blue: platformColor(systemBlue);
--sf-green: platformColor(systemGreen);
--sf-red: platformColor(systemRed);
--sf-gray: platformColor(systemGray);
--sf-text: platformColor(label);
--sf-text-2: platformColor(secondaryLabel);
--sf-bg: platformColor(systemBackground);
--sf-bg-2: platformColor(secondarySystemBackground);
}
}
/* Register as Tailwind theme colors */
@layer theme {
@theme {
--color-sf-blue: var(--sf-blue);
--color-sf-green: var(--sf-green);
--color-sf-red: var(--sf-red);
--color-sf-gray: var(--sf-gray);
--color-sf-text: var(--sf-text);
--color-sf-text-2: var(--sf-text-2);
--color-sf-bg: var(--sf-bg);
--color-sf-bg-2: var(--sf-bg-2);
}
}
続いて、コンポーネントで使用します。
<Text className="text-sf-text">Primary text</Text>
<Text className="text-sf-text-2">Secondary text</Text>
<View className="bg-sf-bg">...</View>
JavaScript で CSS 変数を使用する
useCSSVariable フックを使用します。
import { useCSSVariable } from "@/tw";
function MyComponent() {
const blue = useCSSVariable("--sf-blue");
return <View style={{ borderColor: blue }} />;
}
NativeWind v4/Tailwind v3 との主な違い
- babel.config.js が不要 - 設定は CSS ファーストになりました
- PostCSS プラグイン -
tailwindcssの代わりに@tailwindcss/postcssを使用します - CSS インポート -
@tailwindディレクティブの代わりに@import "tailwindcss/..."を使用します - テーマ設定 -
tailwind.config.jsの代わりに CSS 内で@themeを使用します - コンポーネントラッパー - className をサポートするには、コンポーネントを
useCssElementでラップする必要があります - Metro の設定 - 異なるオプション(
inlineVariables: false)を指定してwithNativewindを使用します
トラブルシューティング
スタイルが適用されない
- アプリのエントリーポイントで CSS ファイルをインポートしていることを確認します
- コンポーネントが
useCssElementでラップされていることを確認します - Metro の設定で
withNativewindが適用されていることを確認します
プラットフォームカラーが機能しない
@media iosブロック内でplatformColor()を使用します- Web/Android では
light-dark()を代替として使用します
TypeScript のエラー
コンポーネントの props に className を追加します。
type Props = React.ComponentProps<typeof RNView> & { className?: string };
制限事項
- タスクが上流の製品または API の対象範囲に明確に合致する場合にのみ、このスキルを使用してください。
- 変更を行う前に、コマンド、API の動作、料金、クォータ、認証情報、デプロイによる影響を、最新の公式ドキュメントで確認してください。
- 生成された例を、環境固有のテスト、セキュリティレビュー、または破壊的もしくは高額な操作に対するユーザーの承認の代わりとして扱わないでください。