| name | expo-tailwind-setup |
| description | Framework (OSS). Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling |
| version | 1.0.0 |
| license | MIT |
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
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:
{
"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:
const { getDefaultConfig } = require("expo/metro-config");
const { withNativewind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
module.exports = withNativewind(config, {
inlineVariables: false,
globalClassNamePolyfill: false,
});
PostCSS Config
Create 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";
@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:
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";
export const Link = (
props: React.ComponentProps<typeof RouterLink> & { className?: string }
) => {
return (, props, { : });
};
. = .;
. = .;
. = .;
. = .;
useCSSVariable =
process.. !==
? useFunctionalVariable
: ;
= .< > & {
?: ;
};
= () => {
(, props, { : });
};
. = ;
= () => {
(, props, { : });
};
. = ;
= () => {
(, props, {
: ,
: ,
});
};
. = ;
= () => {
(, props, { : });
};
. = ;
= () => {
(, props, { : });
};
. = ;
= () => {
(., props, {
: ,
: ,
: ,
});
};
() {
{ underlayColor, ...style } = .(props.) || {};
(
);
}
= () => {
(, props, { : });
};
. = ;
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>) {
const { objectFit, objectPosition, ...style } =
StyleSheet.flatten(props.style) || {};
return (
<AnimatedExpoImage
contentFit={objectFit}
=
{}
=
=== ? { }
}
// @
=
/>
);
}
= () => {
(, props, { : });
};
. = ;
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 {
--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:
@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:
@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));
: (( ), ( ));
: (( / ), ( / ));
: (( ), ( ));
: (( ), ( ));
}
ios {
{
: (systemBlue);
: (systemGreen);
: (systemRed);
: (systemGray);
: (label);
: (secondaryLabel);
: (systemBackground);
: (secondarySystemBackground);
}
}
theme {
{
: (--sf-blue);
: (--sf-green);
: (--sf-red);
: (--sf-gray);
: (--sf-text);
: (--sf-text-);
: (--sf-bg);
: (--sf-bg-);
}
}
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 };
Submitting Feedback
If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:
npx --yes submit-expo-feedback@latest --category skills --subject "expo-tailwind-setup" "<actionable feedback>"
Only submit when you have something specific and actionable to report. Include as much relevant context as possible.
If an AI agent repeatedly failed or the user had to take over an Expo task, load the expo-skill-feedback skill and follow its eval-candidate flow instead of reusing the command above.