Skip to content

Fix: NativeWind Not Working — Styles Not Applying, Dark Mode Broken, or Metro Bundler Errors

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix NativeWind issues — Tailwind CSS for React Native setup, Metro bundler configuration, className prop, dark mode, responsive styles, and Expo integration.

The Problem

Tailwind classes have no effect on React Native components:

import { View, Text } from 'react-native';

function App() {
  return (
    <View className="flex-1 items-center justify-center bg-blue-500">
      <Text className="text-white text-2xl font-bold">Hello</Text>
    </View>
  );
}
// No styles applied — plain unstyled view

Or Metro bundler throws an error:

error: SyntaxError: Unexpected token (NativeWind CSS)

Or dark mode classes don’t work:

<View className="bg-white dark:bg-gray-900">
// Always shows white — dark mode is ignored

Why This Happens

NativeWind bridges Tailwind CSS and React Native. It compiles Tailwind utilities into React Native StyleSheet objects at build time, so the runtime cost is roughly the same as writing the StyleSheet by hand. The setup is the part where things go wrong.

NativeWind v4 requires a Metro CSS transformer. Metro is React Native’s bundler, and out of the box it does not understand .css files. The withNativeWind wrapper in metro.config.js plugs in the transformer that reads global.css, runs Tailwind’s PostCSS pipeline against your tailwind.config.js, and produces the generated utilities. If that wrapper is missing or global.css is not imported at the entry point, your className props are stripped and you get a plain unstyled view. The same failure mode looks like “everything broke after I upgraded Expo SDK” because new SDKs ship new Metro versions and stale metro.config.js files don’t pick up the right transformer.

Tailwind must be configured with the right content paths. tailwind.config.js needs a content array that scans every file containing a className. If you have routes under app/, components under components/, and shared code under src/, all three need to be in content. Tailwind treats anything outside that list as if it didn’t exist — the utility classes it generates only cover classes it found at scan time. Adding a new directory after the fact and not updating content produces the classic “this one file is unstyled” bug.

className works on core React Native components in v4 but third-party libraries need help. View, Text, Pressable, ScrollView, and the rest of react-native are remapped automatically. Components from react-native-safe-area-context, expo-blur, react-native-svg, and similar packages are not — you have to call cssInterop(Component, { className: 'style' }) once, usually in a top-level setup file, before they accept Tailwind classes.

Dark mode uses React Native’s color scheme rather than CSS media queries. The dark: variant fires when useColorScheme() returns 'dark', which by default mirrors the OS setting. If you want manual control, call setColorScheme('dark') from nativewind’s useColorScheme hook — not from the react-native one with the same name.

Version History: NativeWind from v2 to v4

NativeWind v2 shipped in 2022 as the first real attempt at “Tailwind for React Native.” It worked by parsing class strings at runtime and converting them to inline styles. That approach was simple to set up but had a performance ceiling — every render did a string parse — and it could not support some Tailwind features (CSS variables, complex selectors, container queries) because there was no actual CSS engine involved. The v2 install was a single dependency and a Babel plugin.

NativeWind v3 was released in July 2023 and changed the architecture entirely. v3 added a build-time compiler that runs the real Tailwind CSS engine against your project’s files and produces React Native StyleSheet objects ahead of time. That fixed the performance issue, brought feature parity with web Tailwind much closer, and introduced the cssInterop pattern for third-party components. v3 is the version most “NativeWind tutorial” blog posts target.

NativeWind v4 went stable in March 2024 and is the version current Expo and React Native CLI templates assume. v4 adds full CSS variable support (so design tokens defined as --color-primary work the same way they do on the web), proper container queries, animation transition utilities, and a tighter Expo SDK integration via the official nativewind/preset Tailwind preset. The migration from v3 to v4 requires moving the tailwindcss peer dependency to v3 (Tailwind itself), updating metro.config.js to use withNativeWind from nativewind/metro, and switching imports for useColorScheme and cssInterop to the new nativewind exports. If your package.json mixes NativeWind 4.x with Tailwind 4.x, the build fails — NativeWind v4 specifically targets Tailwind CSS 3.x; Tailwind 4 support is tracked separately. v4 also requires react-native-reanimated to be installed (even if you don’t use animations directly) because the transition utilities lean on it.

The closest peers are tailwind-rn (the original Tailwind-on-RN library, now in maintenance mode), twrnc (a runtime parser that is simpler to install but slower), and Tamagui (a different model entirely — a compiler that produces atomic styles from its own DSL, not from Tailwind class strings). Choose NativeWind v4 when you want Tailwind specifically and your team already knows it from the web; choose Tamagui when you want a design system with first-class theme tokens and don’t need Tailwind class names.

The Expo SDK integration is the part that improved the most across versions. NativeWind v2 required manual Babel and Metro configuration that broke on every Expo SDK upgrade. v3 added the nativewind/preset Tailwind preset and the nativewind/metro Metro wrapper, which made the SDK upgrade story tolerable. v4 went further: the official Expo template (npx create-expo-app --template) has a NativeWind option that wires up tailwind.config.js, global.css, metro.config.js, and the nativewind-env.d.ts reference in a single command. If you started a project before that template existed and the setup looks different from current tutorials, the easiest fix is to regenerate the template into a new directory, copy your app/ and components/ directories over, and re-do the config alignment in the new tree.

Dark mode behavior also shifted across versions. v2 used a runtime context provider that you had to render at the root. v3 introduced the useColorScheme hook from nativewind that mirrors react-native’s useColorScheme but adds setColorScheme and toggleColorScheme. v4 kept that hook but reworked the under-the-hood implementation so the dark: variant compiles into a single Appearance.getColorScheme() check at render time instead of a context subscription. The practical impact: in v4 you can call setColorScheme('dark') from anywhere and the entire tree updates without needing a provider, but the hook must be imported from nativewind, not react-native. Mixing the two imports is the source of “dark mode toggles in my component but does not propagate to the rest of the app” reports.

Fix 1: Setup NativeWind v4 with Expo

npx expo install nativewind tailwindcss react-native-reanimated
npx expo install -- --save-dev prettier-plugin-tailwindcss
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './app/**/*.{js,jsx,ts,tsx}',
    './components/**/*.{js,jsx,ts,tsx}',
    './src/**/*.{js,jsx,ts,tsx}',
  ],
  presets: [require('nativewind/preset')],
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#64748b',
      },
    },
  },
  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' });
// app/_layout.tsx (Expo Router) or App.tsx
import '../global.css';  // Import CSS at the entry point

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
    </Stack>
  );
}
// nativewind-env.d.ts — TypeScript support
/// <reference types="nativewind/types" />

Fix 2: Use Tailwind Classes

import { View, Text, Pressable, ScrollView, Image } from 'react-native';

function HomeScreen() {
  return (
    <ScrollView className="flex-1 bg-white dark:bg-gray-900">
      {/* Layout */}
      <View className="px-4 py-6">
        <Text className="text-3xl font-bold text-gray-900 dark:text-white">
          Dashboard
        </Text>
        <Text className="text-gray-500 dark:text-gray-400 mt-1">
          Welcome back!
        </Text>
      </View>

      {/* Card */}
      <View className="mx-4 p-4 bg-white dark:bg-gray-800 rounded-2xl shadow-md border border-gray-100 dark:border-gray-700">
        <View className="flex-row items-center gap-3">
          <Image
            source={{ uri: 'https://example.com/avatar.jpg' }}
            className="w-12 h-12 rounded-full"
          />
          <View className="flex-1">
            <Text className="text-lg font-semibold text-gray-900 dark:text-white">
              Alice Johnson
            </Text>
            <Text className="text-sm text-gray-500">[email protected]</Text>
          </View>
        </View>
      </View>

      {/* Button */}
      <Pressable className="mx-4 mt-4 bg-primary py-3 rounded-xl active:opacity-80">
        <Text className="text-white text-center font-semibold text-lg">
          Continue
        </Text>
      </Pressable>

      {/* Responsive — platform-specific */}
      <View className="mt-4 px-4 ios:pb-8 android:pb-4">
        <Text className="text-sm text-gray-400">
          Platform-specific padding
        </Text>
      </View>
    </ScrollView>
  );
}

Fix 3: Dark Mode

import { useColorScheme } from 'nativewind';
import { View, Text, Pressable } from 'react-native';

function SettingsScreen() {
  const { colorScheme, setColorScheme, toggleColorScheme } = useColorScheme();

  return (
    <View className="flex-1 bg-white dark:bg-gray-900 p-4">
      <Text className="text-xl font-bold text-gray-900 dark:text-white">
        Appearance
      </Text>

      <View className="mt-4 gap-2">
        <Pressable
          onPress={() => setColorScheme('light')}
          className={`p-4 rounded-xl border ${
            colorScheme === 'light'
              ? 'border-primary bg-blue-50 dark:bg-blue-900/20'
              : 'border-gray-200 dark:border-gray-700'
          }`}
        >
          <Text className="font-medium text-gray-900 dark:text-white">Light</Text>
        </Pressable>

        <Pressable
          onPress={() => setColorScheme('dark')}
          className={`p-4 rounded-xl border ${
            colorScheme === 'dark'
              ? 'border-primary bg-blue-50 dark:bg-blue-900/20'
              : 'border-gray-200 dark:border-gray-700'
          }`}
        >
          <Text className="font-medium text-gray-900 dark:text-white">Dark</Text>
        </Pressable>

        <Pressable
          onPress={() => setColorScheme('system')}
          className="p-4 rounded-xl border border-gray-200 dark:border-gray-700"
        >
          <Text className="font-medium text-gray-900 dark:text-white">System</Text>
        </Pressable>
      </View>
    </View>
  );
}

Fix 4: Third-Party Component Styling

// Third-party components don't support className by default
// Use cssInterop to add className support

import { cssInterop } from 'nativewind';
import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Path } from 'react-native-svg';
import { BlurView } from 'expo-blur';

// Remap className to the component's style prop
cssInterop(SafeAreaView, { className: 'style' });
cssInterop(BlurView, { className: 'style' });
cssInterop(Svg, { className: { target: 'style', nativeStyleToProp: { width: true, height: true } } });

// Now you can use className on these components
<SafeAreaView className="flex-1 bg-white">
  <BlurView className="absolute inset-0" intensity={80} />
</SafeAreaView>

Fix 5: Custom Components

// Reusable styled components
import { View, Text, Pressable, type PressableProps } from 'react-native';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

// cn utility
export function cn(...inputs: (string | undefined | false)[]) {
  return inputs.filter(Boolean).join(' ');
}

// Button with variants
const buttonVariants = cva(
  'flex-row items-center justify-center rounded-xl',
  {
    variants: {
      variant: {
        default: 'bg-primary active:bg-blue-600',
        secondary: 'bg-gray-100 dark:bg-gray-800 active:bg-gray-200',
        destructive: 'bg-red-500 active:bg-red-600',
        outline: 'border-2 border-gray-300 dark:border-gray-600 active:bg-gray-50',
        ghost: 'active:bg-gray-100 dark:active:bg-gray-800',
      },
      size: {
        sm: 'px-3 py-2',
        md: 'px-4 py-3',
        lg: 'px-6 py-4',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
);

const buttonTextVariants = cva('font-semibold text-center', {
  variants: {
    variant: {
      default: 'text-white',
      secondary: 'text-gray-900 dark:text-white',
      destructive: 'text-white',
      outline: 'text-gray-900 dark:text-white',
      ghost: 'text-gray-900 dark:text-white',
    },
    size: {
      sm: 'text-sm',
      md: 'text-base',
      lg: 'text-lg',
    },
  },
  defaultVariants: { variant: 'default', size: 'md' },
});

interface ButtonProps extends PressableProps, VariantProps<typeof buttonVariants> {
  title: string;
  className?: string;
}

export function Button({ title, variant, size, className, ...props }: ButtonProps) {
  return (
    <Pressable className={cn(buttonVariants({ variant, size }), className)} {...props}>
      <Text className={buttonTextVariants({ variant, size })}>{title}</Text>
    </Pressable>
  );
}

// Usage
<Button title="Sign In" variant="default" size="lg" />
<Button title="Cancel" variant="outline" />
<Button title="Delete" variant="destructive" />

Fix 6: Animations and Transitions

// NativeWind v4 supports transition utilities with Reanimated
import { View, Pressable, Text } from 'react-native';

function AnimatedCard() {
  const [expanded, setExpanded] = useState(false);

  return (
    <Pressable onPress={() => setExpanded(!expanded)}>
      <View
        className={cn(
          'bg-white dark:bg-gray-800 rounded-2xl p-4 transition-all duration-300',
          expanded ? 'shadow-xl scale-[1.02]' : 'shadow-md',
        )}
      >
        <Text className="font-bold text-lg text-gray-900 dark:text-white">
          Tap to expand
        </Text>
        {expanded && (
          <Text className="mt-2 text-gray-600 dark:text-gray-400">
            Additional content shown when expanded.
          </Text>
        )}
      </View>
    </Pressable>
  );
}

Still Not Working?

Classes have no effect — the Metro config is missing withNativeWind. Add it to metro.config.js and restart Metro: npx expo start --clear. Also import global.css in your root layout.

“Unexpected token” in Metro bundler — Metro can’t process CSS without the NativeWind transformer. Ensure withNativeWind(config, { input: './global.css' }) wraps your Metro config. Clear the cache: npx expo start --clear.

Dark mode doesn’t workdark: classes require the system to be in dark mode, or use setColorScheme('dark') from useColorScheme(). NativeWind respects the system setting by default. To override, wrap your app in a color scheme provider.

Third-party components ignore className — only React Native core components support className out of the box. Use cssInterop() to add support to third-party components like SafeAreaView, BlurView, etc.

Worked in v3, broken after upgrading to v4 — v4 changed several imports and required react-native-reanimated. Install it (npx expo install react-native-reanimated), switch useColorScheme and cssInterop imports to come from nativewind instead of older paths, and confirm metro.config.js uses withNativeWind from nativewind/metro. The most subtle break is the tailwindcss version — NativeWind v4 needs Tailwind 3.x, not Tailwind 4.x, and the resolver does not catch the mismatch as a clear error.

One file is unstyled, the rest works — Tailwind’s content array doesn’t include that file’s directory. Add the missing pattern (for example './features/**/*.{ts,tsx}') to tailwind.config.js and restart Metro with --clear. Tailwind only generates utilities for classes it finds during the scan, so a directory that isn’t in content is invisible.

Production build strips styles that work in dev — Metro’s production tree-shaker drops unreferenced styles. If you build class names dynamically (bg-${color}-500), the scanner can’t see them and Tailwind doesn’t generate them. Either list the dynamic classes in safelist in tailwind.config.js or use complete class names in conditional expressions instead of string concatenation.

For related mobile issues, see Fix: Expo Not Working, Fix: React Native Reanimated Not Working, Fix: Expo Router Not Working, and Fix: React Native Metro Bundler Failed.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles