Communitygithub.com

Samueladewole/claude-skills

在 Expo 專案中配置 Tailwind CSS v4,結合 react-native-css 和 NativeWind v5,實現跨 iOS、Android 與 Web 的統一樣式設計。

claude-skills 是什麼?

這個技能提供了一份完整的指南,幫助你在 Expo 專案中設定 Tailwind CSS v4,並整合 react-native-css 和 NativeWind v5。它詳細說明了安裝相依套件、設定 Metro 與 PostCSS 配置、建立全域 CSS 檔案,以及如何透過 CSS 元件包裝器(如 View、Text、Image)來啟用 className 支援。此外,還涵蓋了自訂主題變數、平台特定樣式(使用媒體查詢)、Apple 系統色彩整合,以及如何使用 useCSSVariable 鉤子在 JavaScript 中存取 CSS 變數。與 NativeWind v4 相比,這個設定不再需要 babel.config.js,改用 CSS-first 的配置方式,讓開發者能更直觀地管理 Tailwind 樣式。此技能支援 iOS、Android 和 Web 平台,並提供了除錯建議與常見問題的解決方案,適合已熟悉 Tailwind CSS 且希望在 React Native 環境中繼續使用其開發體驗的開發者。

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

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

在 Expo 中設定 Tailwind CSS v4(搭配 react-native-css)

本指南說明如何在 Expo 中使用 react-native-css 和 NativeWind v5 設定 Tailwind CSS v4,以實現 iOS、Android 和 Web 的統一樣式。

總覽

此設定使用:

  • Tailwind CSS v4 - 現代化 CSS-first 配置
  • 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 相容性新增 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, {
  // 內聯變數會破壞 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 時,你不需要 babel.config.js 來處理 Tailwind。如果存在 NativeWind babel presets,請將其移除:

// 如果 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)";

動畫元件(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>
  );
}

自訂主題變數

在 global.css 中使用 @theme 加入自訂主題變數:

@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">主要文字</Text>
<Text className="text-sf-text-2">次要文字</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-first
  2. PostCSS 外掛 - 使用 @tailwindcss/postcss 取代 tailwindcss
  3. CSS 匯入 - 使用 @import "tailwindcss/..." 取代 @tailwind 指令
  4. 主題配置 - 在 CSS 中使用 @theme 取代 tailwind.config.js
  5. 元件包裝器 - 必須使用 useCssElement 包裝元件才能支援 className
  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 };

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.

相關技能