NativeWind (Tailwind for RN)
Utility classes, dark mode, and build setup on Expo.
Search across all documentation pages
Utility classes, dark mode, and build setup on Expo.
NativeWind brings Tailwind CSS ergonomics to React Native: className="flex-1 p-4 bg-white dark:bg-slate-900" instead of sprawling StyleSheet objects. On Expo SDK 57, NativeWind v4 hooks into Metro and compiles utilities to RN styles at build time - not a runtime CSS parser on device.
Quick-reference recipe card - copy-paste ready.
npx create-expo-app@latest MyWindApp --template blank-typescript
cd MyWindApp
npx expo install nativewind tailwindcss@^3.4.0 react-native-reanimated react-native-safe-area-context
npm install --save-dev prettier-plugin-tailwindcss
npx tailwindcss init// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
brand: { DEFAULT: "#2563eb", muted: "#93c5fd" },
},
},
},
plugins: [],
};/* global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: "./global.css" });// nativewind-env.d.ts
/// <reference types="nativewind/types" />// app/_layout.tsx (Expo Router) or App.tsx
import "../global.css";
import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}// components/Card.tsx
import { Text, View } from "react-native";
export function Card({ title, body }: { title: string; body: string }) {
return (
<View className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-700 dark:bg-slate-900">
<Text className="text-base font-semibold text-slate-900 dark:text-slate-50">{title}</Text>
<Text className="mt-1 text-sm text-slate-600 dark:text-slate-400">{body}</Text>
</View>
);
}When to reach for this:
StyleSheet duplication is slowing velocity.dark: variants tied to system scheme without hand-maintaining two palettes per component.md:, lg:) for tablet layouts alongside phone defaults.Settings screen with system dark mode, semantic brand tokens, and a pressed-state button.
// app/settings.tsx
import { Pressable, ScrollView, Text, useColorScheme, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { StatusBar } from "expo-status-bar";
const ROWS = [
{ id: "profile", label: "Edit profile" },
{ id: "notifications", label: "Notifications" },
{ id: "privacy", label: "Privacy" },
] as const;
export default function SettingsScreen() {
const scheme = useColorScheme() ?? "light";
return (
<SafeAreaView className="flex-1 bg-slate-50 dark:bg-slate-950">
<StatusBar style={scheme === "dark" ? "light" : "dark"} />
<ScrollView contentContainerClassName="gap-3 p-4">
<Text className="text-2xl font-bold text-slate-900 dark:text-slate-50">Settings</Text>
<View className="overflow-hidden rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
{ROWS.map((row) => (
<Pressable
key={row.id}
accessibilityRole="button"
className="border-b border-slate-100 px-4 py-3.5 active:bg-slate-100 dark:border-slate-800 dark:active:bg-slate-800"
>
<Text className="text-base text-slate-900 dark:text-slate-100">{row.label}</Text>
</Pressable>
))}
</View>
<Pressable
accessibilityRole="button"
className="items-center rounded-xl bg-brand px-4 py-3.5 active:opacity-80"
>
<Text className="text-base font-semibold text-white">Save changes</Text>
</Pressable>
</ScrollView>
</SafeAreaView>
);
}// tailwind.config.js - darkMode for class-based dark: utilities
module.exports = {
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
darkMode: "class",
theme: {
extend: {
colors: {
brand: { DEFAULT: "#2563eb", foreground: "#ffffff" },
},
},
},
};// providers/ThemeModeProvider.tsx - optional in-app override
import { createContext, useContext, useEffect, useState } from "react";
import { Appearance, useColorScheme } from "react-native";
import { colorScheme } from "nativewind";
type Mode = "light" | "dark" | "system";
const ThemeModeContext = createContext<{ mode: Mode; setMode: (m: Mode) => void }>({
mode: "system",
setMode: () => {},
});
export function ThemeModeProvider({ children }: { children: React.ReactNode }) {
const system = useColorScheme();
const [mode, setMode] = useState<Mode>("system");
useEffect(() => {
if (mode === "system") {
Appearance.setColorScheme(null);
colorScheme.set(system === "dark" ? "dark" : "light");
} else {
Appearance.setColorScheme(mode);
colorScheme.set(mode);
}
}, [mode, system]);
return (
<ThemeModeContext.Provider value={{ mode, setMode }}>{children}</ThemeModeContext.Provider>
);
}
export function useThemeMode() {
return useContext(ThemeModeContext);
}What this demonstrates:
withNativeWind in Metro compiles global.css and wires className on RN primitives.dark: variants respond to NativeWind's colorScheme API synced with useColorScheme.contentContainerClassName on ScrollView applies utilities to the scroll content wrapper.brand color in theme.extend - screens use bg-brand, not raw hex.active: pseudo-utility for press feedback without inline StyleSheet state.nativewind/preset maps web Tailwind utilities to RN-supported properties - unsupported CSS is stripped or warned in dev.className is not a native prop - NativeWind patches supported components (View, Text, Pressable, etc.) via the jsxImportSource or Babel plugin depending on setup.darkMode: "class", call colorScheme.set("dark") from nativewind when overriding system appearance; dark: classes activate accordingly.tailwind.config.js theme.extend; screens consume utilities, not one-off text-[#334155] arbitrary values.| Concern | StyleSheet + tokens | NativeWind |
|---|---|---|
| Learning curve | RN-native | Tailwind vocabulary |
| Bundle overhead | Minimal | Config + compiler floor |
| Dark mode | Manual theme object | dark: variants |
| List hot paths | Stable references easy | Keep class strings static |
| Designer handoff | Token table | Utility class spec |
// nativewind-env.d.ts
/// <reference types="nativewind/types" />className is typed on RN components.className, use cssInterop from NativeWind to map className → style.renderItem - dynamic template literals defeat compile-time extraction.// app/_layout.tsx
import "../global.css";
import { ThemeModeProvider } from "../providers/ThemeModeProvider";
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<ThemeModeProvider>
<Stack />
</ThemeModeProvider>
);
}Import global.css once at the root layout - child routes inherit compiled utilities.
Missing content paths - Tailwind purges unused classes; if ./app/** is omitted, styles silently disappear. Fix: Include every folder with className strings.
Forgot global.css import - Utilities never register. Fix: Import at root App.tsx or app/_layout.tsx.
Metro cache stale after config change - Old styles persist. Fix: npx expo start --clear after tailwind.config.js edits.
Arbitrary values everywhere - p-[13px] bypasses your scale. Fix: Extend theme.spacing and enforce in code review.
Dynamic class concatenation in lists - `bg-${color}-500` breaks compile-time extraction. Fix: Use a fixed map: const bg = { red: "bg-red-500", blue: "bg-blue-500" }.
dark: without syncing colorScheme - In-app toggle changes Appearance but not NativeWind. Fix: Call colorScheme.set() in your theme provider.
Third-party TextInput ignores className - Not all components are patched. Fix: cssInterop(TextInput, { className: "style" }) in a setup file.
| Alternative | Use When | Don't Use When |
|---|---|---|
| NativeWind v4 | Tailwind team, many screens, dark variants | Two-screen app with five styles |
| StyleSheet + tokens | Minimal deps, full control | Designers mandate utility workflow |
| Tamagui | Compiled tokens + components + web | You only want class strings, not a component kit |
| React Native Paper | Material components out of the box | Custom brand unrelated to MD3 |
| Unistyles | Runtime themes with breakpoints | Team has zero RN styling library experience |
Yes. Install via npx expo install nativewind tailwindcss react-native-reanimated, wrap Metro with withNativeWind, and import global.css at the app root. Clear Metro cache after config changes.
In tailwind.config.js under theme.extend - colors, spacing, fontSize, borderRadius. Screens reference text-brand, p-md, not raw values. Align names with Design Systems Basics.
Set darkMode: "class" and use dark:bg-slate-900 utilities. Sync with system via useColorScheme, or override with colorScheme.set("dark") from nativewind when the user picks an in-app theme. See Dark Mode & Color Schemes.
Yes. Import global.css in app/_layout.tsx. Add ./app/**/* to Tailwind content paths.
Treat className like a base layer and pass style for truly dynamic values (animated width). Avoid duplicating the same property in both.
NativeWind targets React Native views. Expo web may render some utilities, but validate layout separately - web-specific issues belong in Styling Basics.
Load fonts with expo-font, then map fontFamily in tailwind.config.js theme.extend. See Typography Scale & Font Loading.
Keep static className strings on row components. Extract rows to memo() children. Profile before assuming utilities are free - see Style Performance.
Appearance APIaccessibilityRole on styled pressablesStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026