Getting started with NativeWind: Tailwind for React Native

It is no secret that Tailwind CSS has found its way into the hearts of many web developers. Its utility-first approach makes it easy for developers to quickly create user interfaces in whichever frontend framework they use. Other perks, like NativeWind’s customizability, consistent design language, and mobile-first responsive design, enhance the overall developer experience. Behind the scenes, this love for Tailwind has seeped into the React Native community, leading to the development of NativeWind — a Tailwind CSS integration for mobile development.

In this article, we’ll learn how to use the NativeWind library to write Tailwind classes in our native apps. To easily follow along with this content, you’ll need:

Some experience developing mobile applications using React Native and Expo would help but isn’t necessary. Now let’s dive in!

How does NativeWind work?

NativeWind, created by Mark Lawlor, is a native library that abstracts Tailwind CSS’s features into a format digestible by mobile applications. Because mobile apps do not have a CSS engine like the browser, NativeWind compiles your Tailwind classes into the supported StyleSheet.create syntax.

Here’s a short snippet of code that shows how components are styled in RN:

jsx
export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
    </View>
  )
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
})

In RN, the className property isn’t supported. However, NativeWind makes it possible to use the className prop with Tailwind classes thanks to some Babel magic.

NativeWind then uses the Tailwind engine to compile and pass them to the NativeWindStyleSheet API, which is a wrapper around StyleSheet.create, to render the styles correctly.

Now that we have a basic understanding of what NativeWind does behind the scenes, let’s install it in a fresh RN application.

Setting up a dev workflow with React Native and Expo

Create a new folder anywhere on your machine and open that directory in your terminal. Then run this command:

bash
npx create-expo-app .

This will create a new React Native project using Expo in your folder. Now, you can start the development server by running this:

bash
npm run start

This command will initiate the Metro bundler, and shortly afterward, a QR code should appear in your terminal.

To view your application on your phone during development, ensure that you have Expo Go installed on your mobile device beforehand. If you're on Android, launch Expo Go and select the "Scan QR code" option. For iOS users, open the camera app and scan the QR code displayed. Once scanned, you'll receive a prompt with a link to open the application in Expo Go.

Installing NativeWind

To add NativeWind to your project, install the nativewind package and its peer dependency tailwindcss:

bash
npm install nativewind && npm install -D [email protected]

We're locking on this specific version of Tailwind CSS to prevent this bug — it should be addressed in NativeWind v4, but more on that later.

Next, run the following command to create a tailwind.config.js file at the root of your project:

bash
npx tailwindcss init

Then, update the content property to specify which directories you want to write Tailwind styles in. In our case, we’ll include the entry file, App.js, and other React components in the components directory — which you can create now:

js
// tailwind.config.js
module.exports = {
  content: ['./App.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

If you’re going to be writing Tailwind styles in other directories, be sure to include them in the content array. Finally, add the Babel plugin for NativeWind to babel.config.js:

js
// babel.config.js
module.exports = function (api) {
  api.cache(true)
  return {
    presets: ['babel-preset-expo'],
    plugins: ['nativewind/babel'],
  }
}

And that’s all it takes to start writing Tailwind CSS classes in your React Native applications! Note that because we have made changes to the Bable config file, we’re required to restart the development server before our changes can apply. Make sure you do so before proceeding.

Creating a simple ecommerce UI with NativeWind

In this article, we’re going to attempt to recreate the UI below using NativeWind and core React Native components. We’ll revise core Tailwind CSS styling concepts like hover effects, fonts, dark mode, and responsive design. We’ll also learn how NativeWind APIs like useColorScheme and platformSelect can be used to create intuitive UIs:

The project assets, including the images and the product data, can all be found in this GitHub repository.

Customizing NativeWind

One of the first things I do when working on a new project that uses Tailwind is to tweak some settings in the config file to match my design. In our case, we can start by adding some extra colors to our app’s interface:

js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./App.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {
      colors: {
        'off-white': {
          DEFAULT: '#E3E3E3',
        },
        dark: {
          DEFAULT: '#1C1C1E',
        },
        'soft-dark': {
          DEFAULT: '#2A2A2F',
        },
      },
    },
  },
  plugins: [],
}

We’ll be using this color palette throughout our application so make sure to update your tailwind.config.js file to match it.

Styling the app layout

You may have observed that the layout we are trying to recreate is a two-column layout. To create this, let’s start by cleaning up the App.js component and setting the flex value of the parent view to 1 so that it takes up the full height of the device’s screen:

jsx
// App.js
import { StatusBar } from 'expo-status-bar'
import { View, SafeAreaView } from 'react-native'
import { products } from './utils/products'

export default function App() {
  return (
    <View className="flex-[1] bg-white pt-8">
      <StatusBar style="auto" />
      <SafeAreaView />
    </View>
  )
}

We’ve also set the background to white and imported the SafeAreaView component — this is an iOS-specific component that ensures the content of our app is not obscured by camera notches, status bars, or other system-provided areas.

With that done, we can proceed to create the two-column layout. We’ll achieve this with React Native’s Flatlist component.

Creating a two-column layout with Flatlist

React Native’s Flatlist component provides a performance-optimized way to render large collections of data with useful features like lazy loading content that's not yet visible in the viewport, a pull-to-refresh functionality, scroll loading, and more.

Using Flatlist in React Native is similar to the way you'd map over some data in React DOM. However, Flatlist handles the iteration for you and exposes some props to help render your layout however you want. There are three important FlatList props to keep in mind:

Import the product data from utils/product.js and render the Flatlist like so:

jsx
// App.js
import { StatusBar } from 'expo-status-bar'
import { FlatList, View, SafeAreaView } from 'react-native'
import { products } from './utils/products'

export default function App() {
  return (
    <View className='flex-[1] bg-white pt-8'>
      <StatusBar style="auto" />
      <SafeAreaView />
      <FlatList
        data={products}
        numColumns={2}
        renderItem={(product_data) => {
          return (
            <View className='justify-center p-3'>
              <Image
                className='m-5 h-56 w-full mx-auto object-cover bg-slate-500 rounded-lg'
                source={product_data.item.image_url}
              />
              <Text className='text-dark mb-3'>
                {product_data.item.name.substring(0, 30) + '...'}
              </Text>
              <Text className='text-dark font-bold'>
                {`$${product_data.item.price}.00`}
              </Text>
            </View>
        }}
        keyExtractor={(item) => {
          return item.key
        }}
      />
    </View>
  )
}

In the snippet above, we've rendered a View that shows the product image, along with its name and price — all styled with Tailwind classes. You'll also notice the numColums prop set to 2, which we use to split the content into both halves of the screen:

Current look of Flatlist with border’s around the layout for clarity

Currently, the widths of these columns aren't constrained the way we'd want them to be. We can fix this by making use of React Native’s Dimensions API to:

  1. Grab the current device's width
  2. Divide its value by the number of columns (2)
  3. Set that result as the width for each column in the Flatlist.

Here's how:

jsx
// App.js
import { StatusBar } from 'expo-status-bar'
import { FlatList, View, Dimensions, SafeAreaView } from 'react-native'
import { products } from './utils/products'
import Product from './components/product'

// calculate the width of each column using the screen dimensions
const numColumns = 2
const screen_width = Dimensions.get('window').width
const column_width = screen_width / numColumns

export default function App() {
  return (
    <View className="flex-[1] bg-white pt-8">
      <StatusBar style="auto" />
      <SafeAreaView />
      <FlatList
        data={products}
        numColumns={numColumns}
        renderItem={(product_data) => {
          return (
            <Product
              image_url={product_data.item.image_url}
              name={product_data.item.name}
              price={product_data.item.price}
              column_width={column_width}
            />
          )
        }}
        keyExtractor={(item) => {
          return item.key
        }}
      />
    </View>
  )
}

Now here’s what we have:

Evenly distributed Flatlist with borders for clarity

For clarity, I've abstracted the product view into a Product component and passed down data from the Flatlist to it as props. Here's the Product component:

jsx
// components/product.jsx
import { View, Text, Image } from 'react-native'

export default function Product({ image_url, name, price, column_width }) {
  return (
    <View style={{ width: column_width }} className="justify-center p-3">
      <Image
        className="m-5 mx-auto h-56 w-full rounded-lg bg-slate-500 object-cover"
        source={image_url}
      />
      <Text className="text-dark mb-3">{name.substring(0, 30) + '...'}</Text>
      <Text className="text-dark font-bold dark:text-white">{`$${price}.00`}</Text>
    </View>
  )
}

Creating the navbar component

Our navbar component will consist of a flexbox layout with some icons. Let’s now install an icon library along with the react-native-svg dependency, which is required to help SVGs render correctly in mobile applications. Run the following command in your terminal:

bash
npm i react-native-heroicons react-native-svg

When the process is complete, you will be able to import icons from the Heroicons library into your React Native project and use props like color and size to change their appearance:

jsx
// components/navbar.jsx
import { Pressable, Platform, View } from 'react-native'
import {
  HomeIcon,
  HeartIcon,
  ShoppingCartIcon,
  SunIcon,
  MoonIcon,
} from 'react-native-heroicons/outline'

export default function Navbar() {
  return (
    <View className="shadow-top flex-row items-center justify-between bg-white px-8 py-6">
      <HomeIcon color="black" size={28} />
      <HeartIcon color="black" size={28} />
      <ShoppingCartIcon color="black" size={28} />
      <Pressable>
        <MoonIcon color="black" size={28} />
      </Pressable>
    </View>
  )
}

In the snippet provided, you'll observe that the View component's flex-direction is explicitly set to row. If you're new to styling in React Native, you might question why this is necessary because row is the default value for the flex-direction property.

However, Flexbox works differently in RN because the default flex-direction of every View component is set to column. This default setting alters the orientation of the main-axis and the cross-axis, therefore changing the meaning of the justify-content and align-items properties.

In our styling, we've intentionally set the flex-direction to row and aligned the icons in the middle. The last spot in the icon list is reserved for the theme icons — sun and moon — wrapped in a Pressable component. This setup lays the foundation for adding the toggle-theme functionality later on.

Now we can import the navbar into App.js:

jsx
// App.js
import { StatusBar } from 'expo-status-bar'
import { FlatList, View, Dimensions, SafeAreaView } from 'react-native'
import { products } from './utils/products'
import Navbar from './components/navbar'
import Product from './components/product'

// calculate the width of each column using the screen dimensions
const numColumns = 2
const screen_width = Dimensions.get('window').width
const column_width = screen_width / numColumns

export default function App() {
  return (
    <View className="flex-[1] bg-white pt-8">
      {/* status bar, safeareaview, flatlist*/}
      <Navbar />
    </View>
  )
}

Platform-specific styling with platformSelect

NativeWind also has a hook called platformSelect, which is a wrapper around React Native’s Platform API. NativeWind allows us to use this hook to apply platform-specific styles in our apps through the Tailwind config file. Let’s see how:

js
// tailwind.config.js
const { platformSelect } = require('nativewind/dist/theme-functions')
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./App.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {
      colors: {
        'platform-color': platformSelect({
          // Now you can provide platform specific values
          ios: 'green',
          android: 'blue',
          default: '#BABABA',
        }),
        dark: {
          DEFAULT: '#1C1C1E',
        },
        'soft-dark': {
          DEFAULT: '#2A2A2F',
        },
      },
    },
  },
  plugins: [],
}

You can then use this class in your components like so:

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

export default function MyComponent() {
  ;<View>
    {/* renders green text on iOS and blue text on Android */}
    <Text className="text-off-white">Platform Specific!</Text>
  </View>
}

In our layout, we're aiming to include a shadow effect on the navbar component. However, because the boxShadow CSS property isn't supported on mobile devices, attempting to configure a Tailwind class for it may not work as expected. As an alternative, we'll use React Native's raw Platform API to achieve this effect:

jsx
// components/navbar.tsx
import { Pressable, Platform, View } from 'react-native'
import {
  HomeIcon,
  HeartIcon,
  ShoppingCartIcon,
  SunIcon,
  MoonIcon,
} from 'react-native-heroicons/outline'

export default function Navbar() {
  return (
    <View
      style={{
{/* use the Platform API to apply shadow styling for different platforms */}
        ...Platform.select({
          ios: {
            shadowColor: 'black',
            shadowOffset: { width: 0, height: -5 },
            shadowOpacity: 0.3,
            shadowRadius: 20,
          },
          android: {
            elevation: 3,
          },
        }),
      }}
      className='px-8 py-6 bg-white shadow-top dark:bg-soft-dark flex-row items-center justify-between'
    >
      <HomeIcon color="black" size={28} />
      <HeartIcon color="black" size={28} />
      <ShoppingCartIcon color="black" size={28} />
      <Pressable>
        <SunIcon color="black" size={28} />
      </Pressable>
    </View>
  )
}

Now, let’s learn how to apply dark mode styles with NativeWind.

Using dark mode in NativeWind

NativeWind supports Tailwind dark mode styling practices seamlessly, allowing you to style your app based on your user’s preferences. With the useColorScheme Hook and the dark: variant selector provided by NativeWind, you can easily write dark mode classes in your React Native application.

The hook returns the current color scheme and methods to set or toggle the color scheme manually between light and dark modes:

jsx
import { useColorScheme } from 'nativewind'

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

  console.log(colorScheme) // 'light' | 'dark'
  setColorScheme('dark')
  toggleColorScheme() // changes colorScheme to opposite of its current value

  return {
    /* ... */
  }
}

We can now use this hook in different parts of this application. Let’s start by creating the toggle color scheme functionality in the navbar when a user clicks on the Pressable element:

jsx
// component/navbar.jsx
import { useColorScheme } from 'nativewind'
import { Pressable, Platform, View } from 'react-native'
import {
  HomeIcon,
  HeartIcon,
  ShoppingCartIcon,
  SunIcon,
  MoonIcon,
} from 'react-native-heroicons/outline'
export default function Navbar() {
  const { colorScheme, toggleColorScheme } = useColorScheme()

  return (
    <View className="shadow-top dark:bg-soft-dark flex-row items-center justify-between bg-white px-8 py-6">
      <HomeIcon color={colorScheme === 'light' ? 'black' : 'white'} size={28} />
      <HeartIcon
        color={colorScheme === 'light' ? 'black' : 'white'}
        size={28}
      />
      <ShoppingCartIcon
        color={colorScheme === 'light' ? 'black' : 'white'}
        size={28}
      />
      <Pressable onPress={toggleColorScheme}>
        {colorScheme === 'light' && (
          <SunIcon
            color={colorScheme === 'light' ? 'black' : 'white'}
            size={28}
          />
        )}
        {colorScheme === 'dark' && (
          <MoonIcon
            color={colorScheme === 'light' ? 'black' : 'white'}
            size={28}
          />
        )}
      </Pressable>
    </View>
  )
}

Clicking the last icon on the navbar should now toggle the theme between light and dark.

Let’s now apply more dark mode classes around our app for a more congruent look. We’ll start with the Product component:

jsx
// components/product.jsx
import { View, Text, Image } from 'react-native'
export default function Product({ image_url, name, price, column_width }) {
  return (
    <View style={{ width: column_width }} className="justify-center p-3">
      <Image
        className="m-5 mx-auto h-56 w-full rounded-lg bg-slate-500 object-cover"
        source={image_url}
      />
      <Text className="text-dark mb-3 dark:text-white">
        {name.substring(0, 30) + '...'}
      </Text>
      <Text className="text-dark font-bold dark:text-white">{`$${price}.00`}</Text>
    </View>
  )
}

Next, we’ll add dark mode classes to the parent View of the entire application. We can also style the appearance of StatusBar to be responsive to the color scheme:

jsx
// App.js
import { StatusBar } from 'expo-status-bar'
import { FlatList, View, Dimensions, SafeAreaView } from 'react-native'
import { products } from './utils/products'
import Navbar from './components/navbar'
import Product from './components/product'
import { useColorScheme } from 'nativewind'

// calculate the width of each column using the screen dimensions
const numColumns = 2
const screen_width = Dimensions.get('window').width
const column_width = screen_width / numColumns

export default function App() {
  const { colorScheme } = useColorScheme()
  return (
    <View className="dark:bg-dark flex-[1] bg-white pt-8">
      <StatusBar style={colorScheme === 'light' ? 'dark' : 'light'} />
      <SafeAreaView />
      <FlatList
        data={products}
        numColumns={numColumns}
        renderItem={(product_data) => {
          return (
            <Product
              image_url={product_data.item.image_url}
              name={product_data.item.name}
              price={product_data.item.price}
              column_width={column_width}
            />
          )
        }}
        keyExtractor={(item) => {
          return item.key
        }}
      />
      <Navbar />
    </View>
  )
}

Limitations of NativeWind compared to Tailwind CSS

While NativeWind brings the convenience of Tailwind CSS to mobile app development, it's important to understand its limitations in comparison to what the tool was originally made for — the web.

Here are some common quirks to keep in mind when using Nativewind:

  1. Native-specific styling: In RN, some styles on particular elements can only be applied through a style object. Take a Flatlist for example — the columnWrapperStyle property can only be applied using a style object and cannot be reached through NativeWind classes. You’ll also encounter many other scenarios where you would have to resort to using the native Stylesheet object so embrace mixing your Nativewind with the style prop
  2. Limited style properties: Some style properties commonly used in web development don’t work as they should. As you’ve seen above, shadows work differently compared to the web

While NativeWind isn’t perfect yet, it does look very promising, especially with a new version around the corner.

The upcoming release of NativeWind 4

The NativeWind open-source community has recently been teasing the release of NativeWind v4. This new version boasts enhancements in both functionality and performance. Notably, it eliminates its dependency on the styled-components package.

Additionally, the Expo team has recruited the creator of NativeWind to enhance Tailwind support within Expo. Sneak peeks into the Nativewind v4 documentation reveal many exciting updates:

Conclusion

NativeWind is doing a great job serving as the bridge connecting Tailwind CSS to mobile applications. NativeWind does a lot of heavy lifting behind the scenes to ensure that the way we write Tailwind CSS for the web is greatly supported on mobile. Check out NativeWind’s official documentation for more information.