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
bash1# 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:
json1// 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:
js1// 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:
js1// postcss.config.mjs 2export default { 3 plugins: { 4 "@tailwindcss/postcss": {}, 5 }, 6};
Global CSS
Create src/global.css:
css1@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:
js1// 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)
tsx1import { 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)
tsx1import { 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)
tsx1import * 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:
tsx1import { 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:
css1@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}
Platform-Specific Styles
Use platform media queries for platform-specific styling:
css1@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:
css1/* 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:
tsx1<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:
tsx1import { 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/postcssinstead oftailwindcss - CSS imports - Use
@import "tailwindcss/..."instead of@tailwinddirectives - Theme config - Use
@themein CSS instead oftailwind.config.js - Component wrappers - Must wrap components with
useCssElementfor className support - Metro config - Use
withNativewindwith 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
withNativewindapplied
Platform colors not working
- Use
platformColor()in@media iosblocks - Fall back to
light-dark()for web/Android
TypeScript errors
Add className to component props:
tsx1type Props = React.ComponentProps<typeof RNView> & { className?: string };