mcpField
E
expo-tailwind-setup-v55

expo-tailwind-setup-v55

github.com/blockmaticデザイン・UI1221 回インストール4 週間前に更新
リポジトリ

expo-tailwind-setup-v55 について

Tailwind CSS v4、react-native-css、NativeWind v5 でクロスプラットフォームのスタイルを設定する Expo 開発者向けガイドです。設定ファイル、CSS 対応ラッパー、CSS 変数、問題の切り分けを説明します。

主な機能

  1. 01Expo で Tailwind CSS v4 を設定
  2. 02Metro と PostCSS の構成
  3. 03className 対応ラッパーの作成
  4. 04JavaScript から CSS 変数を取得
  5. 05スタイルと TypeScript の問題を調査

活用シーン

  1. 01Expo プロジェクトに Tailwind スタイルを設定
  2. 02iOS、Android、Web でスタイルを共有
  3. 03React Native コンポーネントに CSS クラスを適用
  4. 04プラットフォームの色やスタイルの問題を診断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/blockmatic/basilic-skills --skill expo-tailwind-setup-v55

インストール後、Claude Code で/expo-tailwind-setup-v55を入力して読み込まれたか確認します。

同梱ファイルagents/openai.yaml

詳細

react-native-css を使用した Expo 向け Tailwind 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 プラグイン

インストール

# 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"
  }
}
  • 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, {
  // 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/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;
  }
}

: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 との主な違い

  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 設定 - オプションが異なる withNativewind(inlineVariables: false)を使用します。

トラブルシューティング

スタイルが適用されない
  1. アプリのエントリーポイントで CSS ファイルをインポートしていることを確認します。
  2. コンポーネントが useCssElement でラップされていることを確認します。
  3. Metro 設定で withNativewind が適用されていることを確認します。
プラットフォームカラーが機能しない
  1. @media ios ブロックで platformColor() を使用します。
  2. Web/Android では light-dark() にフォールバックします。
TypeScript エラー

コンポーネントの props に className を追加します。

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

フィードバックの送信

このスキルにエラー、誤解を招く情報、または古い情報がある場合は、Expo が改善できるよう報告してください。

npx --yes submit-expo-feedback@latest --category skills --subject "expo-tailwind-setup" "<actionable feedback>"

具体的かつ実行可能な報告内容がある場合にのみ送信してください。関連するコンテキストをできるだけ詳しく含めてください。 AI エージェントが繰り返し失敗した場合や、ユーザーが Expo のタスクを引き継ぐ必要があった場合は、上記のコマンドを再利用するのではなく、expo-skill-feedback スキルを読み込み、その eval-candidate フローに従ってください。

コメント