mcpField
E
expo-tailwind-setup

expo-tailwind-setup

github.com/sickn33开发48.7k62 次安装1 天前更新
代码仓库

关于 expo-tailwind-setup

用于在 Expo 应用中搭配 react-native-css 和 NativeWind v5 使用 Tailwind CSS v4 的配置技能。内容涵盖 iOS、Android 和 Web 的设置,包括 CSS 包装组件及与早期版本的差异。

主要功能

  1. 01在 Expo 中配置 Tailwind CSS v4
  2. 02设置 Metro 和 PostCSS
  3. 03创建 CSS 包装组件
  4. 04在 JavaScript 中使用 CSS 变量
  5. 05应用平台专属样式
  6. 06排查样式和 TypeScript 问题

适用场景

  1. 01为新的 Expo 应用配置通用样式
  2. 02从 Tailwind v3 迁移配置
  3. 03为 React Native 组件添加 className 支持
  4. 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

适用场景

当你需要在 Expo 中使用 react-native-css 和 NativeWind v5 设置 Tailwind CSS v4,以实现通用样式时,请使用此技能。

本指南介绍如何在 Expo 中使用 react-native-css 和 NativeWind v5 设置 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 预览版/nightly 配置方案。更改现有应用前,请确认当前 Expo SDK、React 和 NativeWind 的兼容性,以及固定版本的依赖 API。合并 Metro/Babel/PostCSS 设置时,不要丢弃无关插件。不同包管理器的 resolutions 行为各异;不要假设 Yarn resolutions 会影响 npm。请验证原生和 Web 构建,以及无障碍/减少动态效果行为。

安装

# Install dependencies
npx expo install tailwindcss@^4 [email protected] [email protected] @tailwindcss/postcss tailwind-merge clsx

添加 resolutions 以确保 lightningcss 兼容:

// package.json
{
  "resolutions": {
    "lightningcss": "1.30.1"
  }
}
  • Expo 中不需要 autoprefixer,因为使用了 lightningcss
  • Expo 默认包含 postcss

配置文件

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。如果使用下方所示的 barrel 导入,请在 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>
  );
}

自定义主题变量

在 global.css 中使用 @theme 添加自定义主题变量:

@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 的主要区别

  1. 无需 babel.config.js - 配置现在以 CSS 为先
  2. PostCSS 插件 - 使用 @tailwindcss/postcss,而不是 tailwindcss
  3. CSS 导入 - 使用 @import "tailwindcss/...",而不是 @tailwind 指令
  4. 主题配置 - 在 CSS 中使用 @theme,而不是 tailwind.config.js
  5. 组件包装器 - 必须使用 useCssElement 包装组件,才能支持 className
  6. Metro 配置 - 使用带有不同选项(inlineVariables: false)的 withNativewind

故障排查

样式未生效
  1. 确保已在应用入口导入 CSS 文件
  2. 检查组件是否已使用 useCssElement 包装
  3. 确认 Metro 配置已应用 withNativewind
平台颜色不起作用
  1. 在 @media ios 代码块中使用 platformColor()
  2. 对 Web/Android 使用 light-dark() 作为回退
TypeScript 错误

将 className 添加到组件属性中:

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

限制

  • 仅当任务明确符合其上游产品或 API 的适用范围时,才使用此技能。
  • 更改前,请根据当前官方文档核实命令、API 行为、定价、配额、凭据和部署影响。
  • 不要将生成的示例视为环境专属测试、安全审查,或在执行破坏性操作或产生费用的操作前征得用户批准的替代品。

评论

支持 Markdown