tailwind-setup
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTailwind CSS Setup for Expo with react-native-css
在Expo中使用react-native-css配置Tailwind CSS
This guide covers setting up Tailwind CSS v4 in Expo using react-native-css and NativeWind v5 for universal styling across iOS, Android, and Web.
本指南介绍如何在Expo中使用react-native-css和NativeWind v5配置Tailwind CSS v4,实现iOS、Android和Web端的跨平台样式开发。
Overview
概述
This setup uses:
- Tailwind CSS v4 - Modern CSS-first configuration
- react-native-css - CSS runtime for React Native
- NativeWind v5 - Metro transformer for Tailwind in React Native
- @tailwindcss/postcss - PostCSS plugin for Tailwind v4
本次配置使用以下工具:
- Tailwind CSS v4 - 现代化的CSS优先配置方案
- react-native-css - React Native的CSS运行时
- NativeWind v5 - 用于React Native中Tailwind的Metro转换器
- @tailwindcss/postcss - Tailwind v4的PostCSS插件
Installation
安装步骤
bash
undefinedbash
undefinedInstall dependencies
安装依赖
npx expo install tailwindcss@^4 nativewind@5.0.0-preview.2 react-native-css@0.0.0-nightly.5ce6396 @tailwindcss/postcss tailwind-merge clsx
Add resolutions for lightningcss compatibility:
```json
// package.json
{
"resolutions": {
"lightningcss": "1.30.1"
}
}- autoprefixer is not needed in Expo because of lightningcss
- postcss is included in expo by default
npx expo install tailwindcss@^4 nativewind@5.0.0-preview.2 react-native-css@0.0.0-nightly.5ce6396 @tailwindcss/postcss tailwind-merge clsx
添加lightningcss兼容性解析配置:
```json
// package.json
{
"resolutions": {
"lightningcss": "1.30.1"
}
}- Expo中无需autoprefixer,因为lightningcss已包含相关功能
- Expo默认已集成postcss
Configuration Files
配置文件
Metro Config
Metro配置
Create or update :
metro.config.jsjs
// 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,
});创建或更新:
metro.config.jsjs
// 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 Config
PostCSS配置
Create :
postcss.config.mjsjs
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};创建:
postcss.config.mjsjs
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};Global CSS
全局CSS
Create :
src/global.csscss
@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;
}
}创建:
src/global.csscss
@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;
}
}IMPORTANT: No Babel Config Needed
重要提示:无需Babel配置
With Tailwind v4 and NativeWind v5, you do NOT need a babel.config.js for Tailwind. Remove any NativeWind babel presets if present:
js
// 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",
// ],
// };
// };使用Tailwind v4和NativeWind v5时,不需要为Tailwind配置babel.config.js。如果之前存在NativeWind的Babel预设,请删除:
js
// 如果babel.config.js仅包含NativeWind配置,请删除该文件
// 以下配置已不再需要:
// module.exports = function (api) {
// api.cache(true);
// return {
// presets: [
// ["babel-preset-expo", { jsxImportSource: "nativewind" }],
// "nativewind/babel",
// ],
// };
// };CSS Component Wrappers
CSS组件包装器
Since react-native-css requires explicit CSS element wrapping, create reusable components:
由于react-native-css需要显式包装CSS元素,因此我们创建可复用的组件:
Main Components (src/tw/index.tsx
)
src/tw/index.tsx核心组件(src/tw/index.tsx
)
src/tw/index.tsxtsx
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)";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",
});
};
// 支持underlayColor提取的TouchableHighlight组件
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 Component (src/tw/image.tsx
)
src/tw/image.tsxImage组件(src/tw/image.tsx
)
src/tw/image.tsxtsx
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)";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 Components (src/tw/animated.tsx
)
src/tw/animated.tsx动画组件(src/tw/animated.tsx
)
src/tw/animated.tsxtsx
import * as TW from "./index";
import RNAnimated from "react-native-reanimated";
export const Animated = {
...RNAnimated,
View: RNAnimated.createAnimatedComponent(TW.View),
};tsx
import * as TW from "./index";
import RNAnimated from "react-native-reanimated";
export const Animated = {
...RNAnimated,
View: RNAnimated.createAnimatedComponent(TW.View),
};Usage
使用方法
Import CSS-wrapped components from your tw directory:
tsx
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>
);
}从tw目录导入支持CSS的组件:
tsx
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>
);
}Custom Theme Variables
自定义主题变量
Add custom theme variables in your global.css using :
@themecss
@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;
}
}在global.css中使用添加自定义主题变量:
@themecss
@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;
}
}Platform-Specific Styles
平台特定样式
Use platform media queries for platform-specific styling:
css
@media ios {
:root {
--font-sans: system-ui;
--font-rounded: ui-rounded;
}
}
@media android {
:root {
--font-sans: normal;
--font-rounded: normal;
}
}使用平台媒体查询实现平台特定样式:
css
@media ios {
:root {
--font-sans: system-ui;
--font-rounded: ui-rounded;
}
}
@media android {
:root {
--font-sans: normal;
--font-rounded: normal;
}
}Apple System Colors with CSS Variables
使用CSS变量实现Apple系统颜色
Create a CSS file for Apple semantic colors:
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);
}
}Then use in components:
tsx
<Text className="text-sf-text">Primary text</Text>
<Text className="text-sf-text-2">Secondary text</Text>
<View className="bg-sf-bg">...</View>创建一个CSS文件定义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);
}
}然后在组件中使用:
tsx
<Text className="text-sf-text">主要文本</Text>
<Text className="text-sf-text-2">次要文本</Text>
<View className="bg-sf-bg">...</View>Using CSS Variables in JavaScript
在JavaScript中使用CSS变量
Use the hook:
useCSSVariabletsx
import { useCSSVariable } from "@/tw";
function MyComponent() {
const blue = useCSSVariable("--sf-blue");
return <View style={{ borderColor: blue }} />;
}使用钩子:
useCSSVariabletsx
import { useCSSVariable } from "@/tw";
function MyComponent() {
const blue = useCSSVariable("--sf-blue");
return <View style={{ borderColor: blue }} />;
}Key Differences from NativeWind v4 / Tailwind v3
与NativeWind v4 / Tailwind v3的主要区别
- No babel.config.js - Configuration is now CSS-first
- PostCSS plugin - Uses instead of
@tailwindcss/postcsstailwindcss - CSS imports - Use instead of
@import "tailwindcss/..."directives@tailwind - Theme config - Use in CSS instead of
@themetailwind.config.js - Component wrappers - Must wrap components with for className support
useCssElement - Metro config - Use with different options (
withNativewind)inlineVariables: false
- 无需babel.config.js - 现在采用CSS优先的配置方式
- PostCSS插件 - 使用替代
@tailwindcss/postcsstailwindcss - CSS导入 - 使用替代
@import "tailwindcss/..."指令@tailwind - 主题配置 - 在CSS中使用替代
@themetailwind.config.js - 组件包装器 - 必须使用包装组件以支持className
useCssElement - Metro配置 - 使用并设置不同选项(
withNativewind)inlineVariables: false
Troubleshooting
故障排除
Styles not applying
样式不生效
- Ensure you have the CSS file imported in your app entry
- Check that components are wrapped with
useCssElement - Verify Metro config has applied
withNativewind
- 确保已在应用入口导入CSS文件
- 检查组件是否已使用包装
useCssElement - 验证Metro配置是否已应用
withNativewind
Platform colors not working
平台颜色不生效
- Use in
platformColor()blocks@media ios - Fall back to for web/Android
light-dark()
- 在块中使用
@media iosplatformColor() - 为Web/Android平台使用作为降级方案
light-dark()
TypeScript errors
TypeScript错误
Add className to component props:
tsx
type Props = React.ComponentProps<typeof RNView> & { className?: string };为组件props添加className类型:
tsx
type Props = React.ComponentProps<typeof RNView> & { className?: string };