Shipnative
ShipnativeShipnative
Sign in
GuideSeptember 2026 · 9 min read

NativeWind: Tailwind in React Native, and When Not To

React Native has no DOM, no stylesheet cascade and no CSS engine, so Tailwind cannot run in it. NativeWind gets you the syntax anyway: you write className, a Babel transform and a small runtime turn those classes into React Native style objects, and your tailwind.config.jstokens work the way they do on the web. It is genuinely good. It is also four config files that must agree with each other, and when one doesn’t, nothing errors — your classes just quietly do nothing. Here’s the setup, the failure modes in order of likelihood, and the cases where plain styles are still the right call.

What it actually does

It is worth being precise, because the mental model prevents most of the bugs. NativeWind does not ship a CSS engine to the device. At build time it reads your source, finds the class names you literally wrote, asks Tailwind what those classes mean, and compiles them into style objects. At runtime a thin layer picks the right object for the current variant state — dark mode, platform, pressed.

Three consequences follow directly from that, and they explain nearly every “why isn’t this working” question: a class the compiler never saw does not exist; a file the compiler never scanned contributes nothing; and CSS that has no React Native equivalent — floats, most selectors, pseudo-elements — has nowhere to land, no matter how valid the Tailwind class is.

The four files

NativeWind v4 is the line most projects are on today, and a v5 tracking Tailwind v4 is landing — pin a version and follow that version’s docs, because the config shape is exactly what changes between them. The v4 setup in an Expo Router project looks like this:

// 1. tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  // Every directory that contains a className. Miss one and those
  // screens render unstyled — with no error anywhere.
  content: [
    './app/**/*.{js,jsx,ts,tsx}',
    './components/**/*.{js,jsx,ts,tsx}',
    './features/**/*.{js,jsx,ts,tsx}',
  ],
  presets: [require('nativewind/preset')],
  theme: {
    extend: {
      colors: { brand: '#fb923c', surface: '#1c1c1c' },
    },
  },
  plugins: [],
};
/* 2. global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
// 3. 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' });
// 4. babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
      'nativewind/babel',
    ],
  };
};

Then import the CSS once, at the root, and add the types so TypeScript stops complaining that classNameisn’t a prop:

// app/_layout.tsx
import '../global.css';
import { Stack } from 'expo-router';

export default function RootLayout() {
  return <Stack />;
}

// nativewind-env.d.ts
/// <reference types="nativewind/types" />

Then restart Metro with the cache cleared.Babel and Metro config are read at startup, so editing them and reloading the app leaves you testing the old bundle and concluding, reasonably but wrongly, that the setup is broken. This one step accounts for a large share of “NativeWind doesn’t work” threads.

The constraint that surprises web developers

Class names are extracted from the literal text of your source. A name assembled at runtime was never seen by the compiler, so it resolves to nothing — silently:

// BROKEN — 'bg-red-500' as a literal never appears in the source.
<View className={`bg-${color}-500`} />

// WORKS — every possible class is written out, so all of them get generated.
const TONE = {
  danger:  'bg-red-500 border-red-600',
  success: 'bg-green-500 border-green-600',
  neutral: 'bg-white/10 border-white/20',
} as const;

<View className={`rounded-xl border p-4 ${TONE[tone]}`} />

The lookup-object pattern is how variant-driven components stay readable, and it is the main reason NativeWind beats StyleSheet on a design system: a button with four tones and three sizes is two small maps, where the StyleSheet version is twelve named entries you have to keep in sync by hand.

Third-party components need mapping

className works on the core components out of the box. A library component only works if it forwards style, and even then you have to register it:

import { cssInterop, remapProps } from 'nativewind';
import { LinearGradient } from 'expo-linear-gradient';
import { FlashList } from '@shopify/flash-list';

// Simple case: className -> style on this component.
cssInterop(LinearGradient, { className: 'style' });

// Multi-prop case: separate classNames for the list and its content container.
remapProps(FlashList, {
  className: 'style',
  contentContainerClassName: 'contentContainerStyle',
});

Components that take colors as their own props rather than through style — icon sets, chart libraries, status bars — can’t be mapped this way at all. Keep those colors in your theme module and pass the value directly, so the Tailwind config stays the single source of truth even where classes can’t reach.

When to use it, and when not

SituationReach forWhy
Shared tokens with a web appNativeWindOne tailwind config, same color and spacing scale on both
Variant-heavy design systemNativeWindClass strings compose; no StyleSheet permutation explosion
Reanimated / per-frame stylesPlain stylesAnimated values need style objects on the UI thread
A five-screen app, soloEitherFour config files may cost more than the styling saves
Heavy third-party UI kitCheck firstEvery wrapped component needs cssInterop mapping

The animation row is the one worth taking seriously. Reanimated drives styles on the UI thread from shared values, and that means style objects, not classes recomputed in React. Mixing is normal and fine: classes for the static layout, an animatedStyle for the parts that move. The animations guide covers why that boundary exists.

The traps

  1. Incomplete content globs. The single most common cause. A new top-level directory of components renders completely unstyled, with no warning at any layer.
  2. Not clearing the Metro cache after a config change. You are testing the previous bundle.
  3. Expecting web CSS to exist. No pseudo-elements, no arbitrary selectors, no position: fixed. Shadows are approximate and differ per platform; check both.
  4. Runtime-built class strings. Silent no-op. Use a lookup map.
  5. Assuming dark mode is free. The dark: variant needs a color scheme source. Wire it to the system setting deliberately — see the dark mode guide.

The shortcut: skip the config archaeology

None of this is conceptually hard. It is four files that have to agree, a cache that has to be cleared, and a set of failure modes that produce no error message — which is a bad combination for an afternoon.

ShipNative generates React Native apps with the styling layer already wired, so you describe the screen and adjust the look in plain language rather than debugging Metro config. When you want the code, export the full Expo project — config files included — and take it from there.

Build it free

Describe your app in one sentence and have it running on your own phone in minutes at shipnative.dev. No credit card, full React Native source export whenever you want it.

Frequently Asked Questions

Can you use Tailwind CSS in React Native?

Not directly — React Native has no DOM and no CSS engine, so Tailwind cannot apply anything. NativeWind is the bridge: you write the same class names, and a Babel transform plus a runtime turn them into React Native style objects at build time. You get Tailwind syntax and your design tokens, not a browser.

Why is my NativeWind className doing nothing?

Four usual causes, in order of frequency: the file is not covered by the content globs in tailwind.config.js, so the class was never generated; the Metro config is not wrapped with withNativeWind pointing at your CSS file; the CSS file with the Tailwind directives is not imported by the root layout; or Metro cached the old bundle. Fix the config, then restart with the cache cleared — a plain reload will keep serving the stale bundle.

Does NativeWind work with third-party components?

Only for components that forward a style prop, and even then you have to tell NativeWind about them. Use cssInterop or remapProps to map className to the right prop on that component. Anything that swallows style — or takes colors as its own props, like many icon and chart libraries — needs a real style object or a token from your theme instead.

Can I build class names dynamically in NativeWind?

No — the class names are extracted statically at build time, so a string you assemble at runtime produces a class that was never generated. Write the full literal in every branch, or map a variant name to a complete class string in a lookup object. This is the same constraint Tailwind has on the web; it just fails more silently here.

Is NativeWind or StyleSheet better?

They solve different problems. NativeWind is worth it when you want one token set shared with a web app, fast iteration, and variant-driven components. StyleSheet stays better for heavily animated views, anything measured per frame, and small apps where four config files is more setup than the styling saves you. Mixing is fine: NativeWind for layout, plain styles for animated values.

Can an AI app builder set this up for me?

Yes. ShipNative generates React Native apps with the styling layer already wired, so you describe the screen rather than the config — and you can export the full Expo project with its config files whenever you want to take it over.

→

React Native Dark Mode

The dark: variant is only half of it — the system listener is the other half.

Read guide →
→

React Native Performance

Where styling costs you frames, and where it does not.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.