Tailwind with NativeWind

How className reaches React Native, what NativeWind 4 compiles it to, and an honest comparison with StyleSheet.

Syntax<View className="flex-1 items-center bg-white dark:bg-black" />

NativeWind lets you write Tailwind utility classes in a React Native app: className="flex-row items-center gap-3" instead of a style object. It exists because a shared vocabulary of utilities is faster to write and easier to keep consistent than a sheet per file, and because the same classes can serve a web build.

What it actually does

There is no CSS engine on the device. At build time NativeWind compiles your Tailwind classes into React Native style objects created with StyleSheet.create, and ships a small runtime that picks between them for conditional variants such as dark: or active:. className reaches the components through a JSX transform rather than a wrapper library, which is why core components work with no extra code. Version 4 is built on Tailwind CSS 3; check the NativeWind docs before pairing it with a newer Tailwind.

Setup

Four files change: babel.config.js gets ["babel-preset-expo", { jsxImportSource: "nativewind" }] plus "nativewind/babel", metro.config.js wraps the config in withNativeWind(config, { input: './global.css' }), global.css holds the three @tailwind directives and is imported once at the app root, and nativewind-env.d.ts adds /// <reference types="nativewind/types" /> so TypeScript accepts className.

The honest comparison

Only utilities that map to a real React Native style prop work. There is no grid, no arbitrary selector, and hover: is close to meaningless on a touch screen. Third-party components ignore className until you register them with cssInterop. And when two utilities conflict, the generated CSS order decides, not the order you typed them. StyleSheet stays the baseline: no build step, no extra dependency, and the types everyone else's examples use.

Example

Example · tsx
import { useState } from 'react';
import { Pressable, Text, View } from 'react-native';
// global.css is imported once at the app root, not here.

type TagProps = {
  label: string;
  selected: boolean;
  onPress: () => void;
};

function Tag({ label, selected, onPress }: TagProps) {
  return (
    <Pressable
      onPress={onPress}
      // Each utility compiles to a StyleSheet entry at build time.
      // dark: is resolved at runtime from the system colour scheme.
      className={`rounded-full px-4 py-2 ${
        selected ? 'bg-sky-600' : 'bg-neutral-200 dark:bg-neutral-800'
      }`}
    >
      <Text
        className={
          selected ? 'text-white' : 'text-neutral-900 dark:text-neutral-100'
        }
      >
        {label}
      </Text>
    </Pressable>
  );
}

export default function NativeWindDemo() {
  const [selected, setSelected] = useState('Swift');
  const tags = ['Swift', 'Kotlin', 'TypeScript'];

  return (
    // flex-row is required: flexDirection still defaults to column here.
    <View className="flex-1 flex-row flex-wrap gap-2 bg-white p-4 dark:bg-black">
      {tags.map((tag) => (
        <Tag
          key={tag}
          label={tag}
          selected={tag === selected}
          onPress={() => setSelected(tag)}
        />
      ))}
    </View>
  );
}

When to use it

  • Style a screen with utilities instead of maintaining a StyleSheet per file.
  • Get dark mode variants for a whole screen by adding dark: classes.
  • Share styling between a React Native app and an Expo web build.
  • Make a className work on a component from a third-party package.

More examples

The same row written twice

Two identical rows, one above the other: a circle and a label on a rounded grey background. Written side by side so the mapping is visible, p-4 is padding: 16 and gap-3 is gap: 12, because Tailwind's spacing scale is 4dp per step. Keep a file like this while a team is learning NativeWind, so nobody has to guess what a utility compiles to.

Example · tsx
import { StyleSheet, Text, View } from 'react-native';

export default function SameRowTwice() {
  return (
    <View>
      {/* NativeWind */}
      <View className="flex-row items-center gap-3 rounded-xl bg-neutral-100 p-4">
        <View className="h-10 w-10 rounded-full bg-neutral-300" />
        <Text className="text-base text-neutral-900">Utilities</Text>
      </View>

      {/* StyleSheet: the same declarations, spelled out */}
      <View style={styles.row}>
        <View style={styles.avatar} />
        <Text style={styles.label}>StyleSheet</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  row: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 12,          // gap-3
    borderRadius: 12, // rounded-xl
    padding: 16,      // p-4
    backgroundColor: '#f5f5f5',
  },
  avatar: { height: 40, width: 40, borderRadius: 20, backgroundColor: '#d4d4d4' },
  label: { fontSize: 16, color: '#171717' },
});

Teaching className to a third-party component

A 48dp circular remote image with a name beside it. expo-image is not a core React Native component, so NativeWind does not know about it until cssInterop maps className onto its style prop. This is the first thing to check when a className appears to be ignored on a component you did not write.

Example · tsx
import { cssInterop } from 'nativewind';
import { Image } from 'expo-image';
import { Text, View } from 'react-native';

// Register once, at module scope: map the className prop onto style.
cssInterop(Image, { className: 'style' });

type AvatarProps = { uri: string; name: string };

export default function Avatar({ uri, name }: AvatarProps) {
  return (
    <View className="flex-row items-center gap-3">
      {/* Without the cssInterop call above, this className would do nothing */}
      <Image source={{ uri }} className="h-12 w-12 rounded-full" />
      <Text className="text-base font-semibold text-neutral-900">{name}</Text>
    </View>
  );
}

Discussion

  • Be the first to comment on this lesson.