Tailwind CSS Setup for Expo with react-native-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.
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
Installation
bash
1# Install dependencies
2npx 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
1// package.json
2{
3 "resolutions": {
4 "lightningcss": "1.30.1"
5 }
6}
- autoprefixer is not needed in Expo because of lightningcss
- postcss is included in expo by default
Configuration Files
Metro Config
Create or update metro.config.js:
js
1// metro.config.js
2const { getDefaultConfig } = require("expo/metro-config");
3const { withNativewind } = require("nativewind/metro");
4
5/** @type {import('expo/metro-config').MetroConfig} */
6const config = getDefaultConfig(__dirname);
7
8module.exports = withNativewind(config, {
9 // inline variables break PlatformColor in CSS variables
10 inlineVariables: false,
11 // We add className support manually
12 globalClassNamePolyfill: false,
13});
PostCSS Config
Create postcss.config.mjs:
js
1// postcss.config.mjs
2export default {
3 plugins: {
4 "@tailwindcss/postcss": {},
5 },
6};
Global CSS
Create src/global.css:
css
1@import "tailwindcss/theme.css" layer(theme);
2@import "tailwindcss/preflight.css" layer(base);
3@import "tailwindcss/utilities.css";
4
5/* Platform-specific font families */
6@media android {
7 :root {
8 --font-mono: monospace;
9 --font-rounded: normal;
10 --font-serif: serif;
11 --font-sans: normal;
12 }
13}
14
15@media ios {
16 :root {
17 --font-mono: ui-monospace;
18 --font-serif: ui-serif;
19 --font-sans: system-ui;
20 --font-rounded: ui-rounded;
21 }
22}
IMPORTANT: No Babel Config Needed
With Tailwind v4 and NativeWind v5, you do NOT need a babel.config.js for Tailwind. Remove any NativeWind babel presets if present:
js
1// DELETE babel.config.js if it only contains NativeWind config
2// The following is NO LONGER needed:
3// module.exports = function (api) {
4// api.cache(true);
5// return {
6// presets: [
7// ["babel-preset-expo", { jsxImportSource: "nativewind" }],
8// "nativewind/babel",
9// ],
10// };
11// };
CSS Component Wrappers
Since react-native-css requires explicit CSS element wrapping, create reusable components:
Main Components (src/tw/index.tsx)
tsx
1import {
2 useCssElement,
3 useNativeVariable as useFunctionalVariable,
4} from "react-native-css";
5
6import { Link as RouterLink } from "expo-router";
7import Animated from "react-native-reanimated";
8import React from "react";
9import {
10 View as RNView,
11 Text as RNText,
12 Pressable as RNPressable,
13 ScrollView as RNScrollView,
14 TouchableHighlight as RNTouchableHighlight,
15 TextInput as RNTextInput,
16 StyleSheet,
17} from "react-native";
18
19// CSS-enabled Link
20export const Link = (
21 props: React.ComponentProps<typeof RouterLink> & { className?: string },
22) => {
23 return useCssElement(RouterLink, props, { className: "style" });
24};
25
26Link.Trigger = RouterLink.Trigger;
27Link.Menu = RouterLink.Menu;
28Link.MenuAction = RouterLink.MenuAction;
29Link.Preview = RouterLink.Preview;
30
31// CSS Variable hook
32export const useCSSVariable =
33 process.env.EXPO_OS !== "web"
34 ? useFunctionalVariable
35 : (variable: string) => `var(${variable})`;
36
37// View
38export type ViewProps = React.ComponentProps<typeof RNView> & {
39 className?: string;
40};
41
42export const View = (props: ViewProps) => {
43 return useCssElement(RNView, props, { className: "style" });
44};
45View.displayName = "CSS(View)";
46
47// Text
48export const Text = (
49 props: React.ComponentProps<typeof RNText> & { className?: string },
50) => {
51 return useCssElement(RNText, props, { className: "style" });
52};
53Text.displayName = "CSS(Text)";
54
55// ScrollView
56export const ScrollView = (
57 props: React.ComponentProps<typeof RNScrollView> & {
58 className?: string;
59 contentContainerClassName?: string;
60 },
61) => {
62 return useCssElement(RNScrollView, props, {
63 className: "style",
64 contentContainerClassName: "contentContainerStyle",
65 });
66};
67ScrollView.displayName = "CSS(ScrollView)";
68
69// Pressable
70export const Pressable = (
71 props: React.ComponentProps<typeof RNPressable> & { className?: string },
72) => {
73 return useCssElement(RNPressable, props, { className: "style" });
74};
75Pressable.displayName = "CSS(Pressable)";
76
77// TextInput
78export const TextInput = (
79 props: React.ComponentProps<typeof RNTextInput> & { className?: string },
80) => {
81 return useCssElement(RNTextInput, props, { className: "style" });
82};
83TextInput.displayName = "CSS(TextInput)";
84
85// AnimatedScrollView
86export const AnimatedScrollView = (
87 props: React.ComponentProps<typeof Animated.ScrollView> & {
88 className?: string;
89 contentClassName?: string;
90 contentContainerClassName?: string;
91 },
92) => {
93 return useCssElement(Animated.ScrollView, props, {
94 className: "style",
95 contentClassName: "contentContainerStyle",
96 contentContainerClassName: "contentContainerStyle",
97 });
98};
99
100// TouchableHighlight with underlayColor extraction
101function XXTouchableHighlight(
102 props: React.ComponentProps<typeof RNTouchableHighlight>,
103) {
104 const { underlayColor, ...style } = StyleSheet.flatten(props.style) || {};
105 return (
106 <RNTouchableHighlight
107 underlayColor={underlayColor}
108 {...props}
109 style={style}
110 />
111 );
112}
113
114export const TouchableHighlight = (
115 props: React.ComponentProps<typeof RNTouchableHighlight>,
116) => {
117 return useCssElement(XXTouchableHighlight, props, { className: "style" });
118};
119TouchableHighlight.displayName = "CSS(TouchableHighlight)";
Image Component (src/tw/image.tsx)
tsx
1import { useCssElement } from "react-native-css";
2import React from "react";
3import { StyleSheet } from "react-native";
4import Animated from "react-native-reanimated";
5import { Image as RNImage } from "expo-image";
6
7const AnimatedExpoImage = Animated.createAnimatedComponent(RNImage);
8
9export type ImageProps = React.ComponentProps<typeof Image>;
10
11function CSSImage(props: React.ComponentProps<typeof AnimatedExpoImage>) {
12 // @ts-expect-error: Remap objectFit style to contentFit property
13 const { objectFit, objectPosition, ...style } =
14 StyleSheet.flatten(props.style) || {};
15
16 return (
17 <AnimatedExpoImage
18 contentFit={objectFit}
19 contentPosition={objectPosition}
20 {...props}
21 source={
22 typeof props.source === "string" ? { uri: props.source } : props.source
23 }
24 // @ts-expect-error: Style is remapped above
25 style={style}
26 />
27 );
28}
29
30export const Image = (
31 props: React.ComponentProps<typeof CSSImage> & { className?: string },
32) => {
33 return useCssElement(CSSImage, props, { className: "style" });
34};
35
36Image.displayName = "CSS(Image)";
Animated Components (src/tw/animated.tsx)
tsx
1import * as TW from "./index";
2import RNAnimated from "react-native-reanimated";
3
4export const Animated = {
5 ...RNAnimated,
6 View: RNAnimated.createAnimatedComponent(TW.View),
7};
Usage
Import CSS-wrapped components from your tw directory:
tsx
1import { View, Text, ScrollView, Image } from "@/tw";
2
3export default function MyScreen() {
4 return (
5 <ScrollView className="flex-1 bg-white">
6 <View className="p-4 gap-4">
7 <Text className="text-xl font-bold text-gray-900">Hello Tailwind!</Text>
8 <Image
9 className="w-full h-48 rounded-lg object-cover"
10 source={{ uri: "https://example.com/image.jpg" }}
11 />
12 </View>
13 </ScrollView>
14 );
15}
Custom Theme Variables
Add custom theme variables in your global.css using @theme:
css
1@layer theme {
2 @theme {
3 /* Custom fonts */
4 --font-rounded: "SF Pro Rounded", sans-serif;
5
6 /* Custom line heights */
7 --text-xs--line-height: calc(1em / 0.75);
8 --text-sm--line-height: calc(1.25em / 0.875);
9 --text-base--line-height: calc(1.5em / 1);
10
11 /* Custom leading scales */
12 --leading-tight: 1.25em;
13 --leading-snug: 1.375em;
14 --leading-normal: 1.5em;
15 }
16}
Use platform media queries for platform-specific styling:
css
1@media ios {
2 :root {
3 --font-sans: system-ui;
4 --font-rounded: ui-rounded;
5 }
6}
7
8@media android {
9 :root {
10 --font-sans: normal;
11 --font-rounded: normal;
12 }
13}
Apple System Colors with CSS Variables
Create a CSS file for Apple semantic colors:
css
1/* src/css/sf.css */
2@layer base {
3 html {
4 color-scheme: light;
5 }
6}
7
8:root {
9 /* Accent colors with light/dark mode */
10 --sf-blue: light-dark(rgb(0 122 255), rgb(10 132 255));
11 --sf-green: light-dark(rgb(52 199 89), rgb(48 209 89));
12 --sf-red: light-dark(rgb(255 59 48), rgb(255 69 58));
13
14 /* Gray scales */
15 --sf-gray: light-dark(rgb(142 142 147), rgb(142 142 147));
16 --sf-gray-2: light-dark(rgb(174 174 178), rgb(99 99 102));
17
18 /* Text colors */
19 --sf-text: light-dark(rgb(0 0 0), rgb(255 255 255));
20 --sf-text-2: light-dark(rgb(60 60 67 / 0.6), rgb(235 235 245 / 0.6));
21
22 /* Background colors */
23 --sf-bg: light-dark(rgb(255 255 255), rgb(0 0 0));
24 --sf-bg-2: light-dark(rgb(242 242 247), rgb(28 28 30));
25}
26
27/* iOS native colors via platformColor */
28@media ios {
29 :root {
30 --sf-blue: platformColor(systemBlue);
31 --sf-green: platformColor(systemGreen);
32 --sf-red: platformColor(systemRed);
33 --sf-gray: platformColor(systemGray);
34 --sf-text: platformColor(label);
35 --sf-text-2: platformColor(secondaryLabel);
36 --sf-bg: platformColor(systemBackground);
37 --sf-bg-2: platformColor(secondarySystemBackground);
38 }
39}
40
41/* Register as Tailwind theme colors */
42@layer theme {
43 @theme {
44 --color-sf-blue: var(--sf-blue);
45 --color-sf-green: var(--sf-green);
46 --color-sf-red: var(--sf-red);
47 --color-sf-gray: var(--sf-gray);
48 --color-sf-text: var(--sf-text);
49 --color-sf-text-2: var(--sf-text-2);
50 --color-sf-bg: var(--sf-bg);
51 --color-sf-bg-2: var(--sf-bg-2);
52 }
53}
Then use in components:
tsx
1<Text className="text-sf-text">Primary text</Text>
2<Text className="text-sf-text-2">Secondary text</Text>
3<View className="bg-sf-bg">...</View>
Using CSS Variables in JavaScript
Use the useCSSVariable hook:
tsx
1import { useCSSVariable } from "@/tw";
2
3function MyComponent() {
4 const blue = useCSSVariable("--sf-blue");
5
6 return <View style={{ borderColor: blue }} />;
7}
Key Differences from NativeWind v4 / Tailwind v3
- No babel.config.js - Configuration is now CSS-first
- PostCSS plugin - Uses
@tailwindcss/postcss instead of tailwindcss
- CSS imports - Use
@import "tailwindcss/..." instead of @tailwind directives
- Theme config - Use
@theme in CSS instead of tailwind.config.js
- Component wrappers - Must wrap components with
useCssElement for className support
- Metro config - Use
withNativewind with different options (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
withNativewind applied
- Use
platformColor() in @media ios blocks
- Fall back to
light-dark() for web/Android
TypeScript errors
Add className to component props:
tsx
1type Props = React.ComponentProps<typeof RNView> & { className?: string };