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
Install 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:
// package.json { "resolutions": { "lightningcss": "1.30.1" } }
-
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 :
// 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 Config
Create postcss.config.mjs :
// postcss.config.mjs export default { plugins: { "@tailwindcss/postcss": {}, }, };
Global CSS
Create 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; } }
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:
// 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 Component Wrappers
Since react-native-css requires explicit CSS element wrapping, create reusable components:
Main Components (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 Component (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)";
Animated Components (src/tw/animated.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:
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 @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;
} }
Platform-Specific Styles
Use platform media queries for platform-specific styling:
@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
Create a CSS file for Apple semantic colors:
/* 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:
<Text className="text-sf-text">Primary text</Text> <Text className="text-sf-text-2">Secondary text</Text> <View className="bg-sf-bg">...</View>
Using CSS Variables in JavaScript
Use the useCSSVariable hook:
import { useCSSVariable } from "@/tw";
function MyComponent() { const blue = useCSSVariable("--sf-blue");
return <View style={{ borderColor: blue }} />; }
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
Platform colors not working
-
Use platformColor() in @media ios blocks
-
Fall back to light-dark() for web/Android
TypeScript errors
Add className to component props:
type Props = React.ComponentProps<typeof RNView> & { className?: string };