# Expo Tailwind Setup

> Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling

- Skill: `involvex/expo-tailwind-setup-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add involvex/expo-tailwind-setup-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/involvex/expo-tailwind-setup-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: involvex (https://skillmd.com/u/involvex)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/involvex/expo-tailwind-setup-2

---


# 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
# 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:

```json
// 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`:

```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`:

```js
// postcss.config.mjs
export default {
	plugins: {
		'@tailwindcss/postcss': {},
	},
}
```

### Global CSS

Create `src/global.css`:

```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:

```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",
//     ],
//   };
// };
```

## CSS Component Wrappers

Since react-native-css requires explicit CSS element wrapping, create reusable components:

### Main Components (`src/tw/index.tsx`)

```tsx
import {
	useCssElement,
	useNativeVariable as useFunctionalVariable,
} from 'react-native-css'

import {
	View as RNView,
	Text as RNText,
	Pressable as RNPressable,
	ScrollView as RNScrollView,
	TouchableHighlight as RNTouchableHighlight,
	TextInput as RNTextInput,
	StyleSheet,
} from 'react-native'
import Animated from 'react-native-reanimated'
import {Link as RouterLink} from 'expo-router'
import React from 'react'

// 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`)

```tsx
import Animated from 'react-native-reanimated'
import {useCssElement} from 'react-native-css'
import {Image as RNImage} from 'expo-image'
import {StyleSheet} from 'react-native'
import React from 'react'

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`)

```tsx
import RNAnimated from 'react-native-reanimated'
import * as TW from './index'

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>
	)
}
```

## Custom Theme Variables

Add custom theme variables in your global.css using `@theme`:

```css
@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:

```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

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>
```

## Using CSS Variables in JavaScript

Use the `useCSSVariable` hook:

```tsx
import {useCSSVariable} from '@/tw'

function MyComponent() {
	const blue = useCSSVariable('--sf-blue')

	return <View style={{borderColor: blue}} />
}
```

## Key Differences from NativeWind v4 / Tailwind v3

1. **No babel.config.js** - Configuration is now CSS-first
2. **PostCSS plugin** - Uses `@tailwindcss/postcss` instead of `tailwindcss`
3. **CSS imports** - Use `@import "tailwindcss/..."` instead of `@tailwind` directives
4. **Theme config** - Use `@theme` in CSS instead of `tailwind.config.js`
5. **Component wrappers** - Must wrap components with `useCssElement` for className support
6. **Metro config** - Use `withNativewind` with different options (`inlineVariables: false`)

## Troubleshooting

### Styles not applying

1. Ensure you have the CSS file imported in your app entry
2. Check that components are wrapped with `useCssElement`
3. Verify Metro config has `withNativewind` applied

### Platform colors not working

1. Use `platformColor()` in `@media ios` blocks
2. Fall back to `light-dark()` for web/Android

### TypeScript errors

Add className to component props:

```tsx
type Props = React.ComponentProps<typeof RNView> & {className?: string}
```

