Communitygithub.com

Tailwind CSS v4 × Expo 環境構築スキル

Expo プロジェクトに Tailwind CSS v4、react-native-css、NativeWind v5 を導入し、iOS・Android・Web で統一的にスタイリングするための設定ガイドです。

Tailwind CSS v4 × Expo 環境構築スキル とは?

このスキルは、Expo アプリケーションで Tailwind CSS v4 を利用するための包括的なセットアップ手順を提供します。react-native-css と NativeWind v5 を組み合わせ、iOS、Android、Web のすべてのプラットフォームで一貫したユーティリティファーストなスタイリングを実現します。具体的には、依存関係のインストール、Metro バンドラの設定、PostCSS の構成、グローバル CSS の作成、そして `useCssElement` を用いたコンポーネントラッパーの実装方法を詳細に解説しています。Tailwind v3 からの主な変更点(Babel 不要、CSS ファーストな設定、`@theme` によるカスタムテーマ、Apple システムカラーの CSS 変数によるサポート)も網羅しており、TypeScript エラーやプラットフォーム固有のスタイルに関するトラブルシューティングも含まれています。

対応✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Samueladewole/claude-skills/tree/HEAD/expo-tailwind-setup

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Tailwind CSS Setup for Expo with react-native-css

このガイドでは、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 プラグイン

インストール

# 依存関係をインストール
npx expo install tailwindcss@^4 [email protected] [email protected] @tailwindcss/postcss tailwind-merge clsx

lightningcss の互換性のために解決バージョンを追加:

// package.json
{
  "resolutions": {
    "lightningcss": "1.30.1"
  }
}
  • Expo では lightningcss を使用するため 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, {
  // インラインフ変数は CSS 変数の PlatformColor を壊す
  inlineVariables: false,
  // className サポートは手動で追加
  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";

/* プラットフォーム固有のフォントファミリー */
@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 プリセットが存在する場合は削除してください:

// babel.config.js が NativeWind 設定のみを含む場合は削除
// 以下はもう必要ありません:
// module.exports = function (api) {
//   api.cache(true);
//   return {
//     presets: [
//       ["babel-preset-expo", { jsxImportSource: "nativewind" }],
//       "nativewind/babel",
//     ],
//   };
// };

CSS コンポーネントラッパー

react-native-css では明示的な CSS 要素ラッピングが必要なため、再利用可能なコンポーネントを作成します:

メインコンポーネント (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 対応 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 変数フック
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(underlayColor 抽出付き)
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: objectFit スタイルを contentFit プロパティに再マッピング
  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={style}
    />
  );
}

export const Image = (
  props: React.ComponentProps<typeof CSSImage> & { className?: string }
) => {
  return useCssElement(CSSImage, props, { className: "style" });
};

Image.displayName = "CSS(Image)";

Animated コンポーネント (src/tw/animated.tsx)

import * as TW from "./index";
import RNAnimated from "react-native-reanimated";

export const Animated = {
  ...RNAnimated,
  View: RNAnimated.createAnimatedComponent(TW.View),
};

使用方法

CSS ラップされたコンポーネントを tw ディレクトリからインポート:

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 {
    /* カスタムフォント */
    --font-rounded: "SF Pro Rounded", sans-serif;

    /* カスタム行高 */
    --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);

    /* カスタム行間スケール */
    --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;
  }
}

:root {
  /* ライト/ダークモード対応のアクセントカラー */
  --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));

  /* グレースケール */
  --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));

  /* テキストカラー */
  --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));

  /* 背景色 */
  --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));
}

/* platformColor による iOS ネイティブカラー */
@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);
  }
}

/* Tailwind テーマカラーとして登録 */
@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 との主な違い

  1. babel.config.js 不要 - 設定は CSS ファースト
  2. PostCSS プラグイン - tailwindcss の代わりに @tailwindcss/postcss を使用
  3. CSS インポート - @tailwind ディレクティブではなく @import "tailwindcss/..." を使用
  4. テーマ設定 - tailwind.config.js ではなく CSS で @theme を使用
  5. コンポーネントラッパー - className サポートには useCssElement でコンポーネントをラップする必要あり
  6. Metro 設定 - 異なるオプション(inlineVariables: false)で withNativewind を使用

トラブルシューティング

スタイルが適用されない

  1. CSS ファイルがアプリのエントリポイントでインポートされていることを確認
  2. コンポーネントが useCssElement でラップされていることを確認
  3. Metro 設定に withNativewind が適用されていることを確認

プラットフォームカラーが動作しない

  1. @media ios ブロック内で platformColor() を使用
  2. Web/Android 用に light-dark() にフォールバック

TypeScript エラー

className をコンポーネントの props に追加:

type Props = React.ComponentProps<typeof RNView> & { className?: string };

Individual skills in this repo

This repo contains 5 individual skills — each has its own dedicated page.

Samueladewole/claude-skills

Create API handoff documentation for frontend developers. Use when backend work is complete and needs to be documented for frontend integration, or user says 'create handoff', 'document API', 'frontend handoff', or 'API documentation'.

Samueladewole/claude-skills

Build accessible, responsive, and performant frontend components with design system best practices, modern CSS, and framework-agnostic patterns.

Samueladewole/claude-skills

Document frontend data needs for backend developers. Use when frontend needs to communicate API requirements to backend, or user says 'backend requirements', 'what data do I need', 'API requirements', or is describing data needs for a UI.

Samueladewole/claude-skills

Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components.

Samueladewole/claude-skills

Analyze a short-form video (TikTok, Instagram Reel, YouTube Short, or any MP4) to extract its full transcript, understand why it performed, and generate concrete repurposing ideas. Use this skill whenever the user asks to: analyze a video, understand why something went viral, reverse-engineer a creator's content, extract a transcript from a video, figure out what makes a video perform, or get repurposing ideas from a video. Also trigger when the user says things like "why did this perform", "what's the hook here", "can I repurpose this", "analyze this TikTok/Reel", or drops a video file path or URL.

関連スキル