DEV Community

Cover image for Getting started with NativeWind: Tailwind for React Native
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Getting started with NativeWind: Tailwind for React Native

Written by Chinwike Maduabuchi
✏️

It is no secret that Tailwind CSS has significantly influenced the web development community. Its utility-first approach makes it easy for developers to quickly create user interfaces in whichever frontend framework they use. Recognizing the value of Tailwind's design philosophy, the community has extended its capabilities to the mobile development sphere with NativeWind.

NativeWind is a Tailwind CSS integration for mobile development, boasting customizability, consistent design language, and mobile-first responsive design. In this article, we’ll learn how to use the NativeWind library to write Tailwind classes in our native apps. To easily follow along, you’ll need:

  • A basic understanding of React
  • Expo Go installed on a physical device
  • Node.js installed on your machine

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:

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',
  },
})
Enter fullscreen mode Exit fullscreen mode

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:

npx create-expo-app .
Enter fullscreen mode Exit fullscreen mode

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

npm run start
Enter fullscreen mode Exit fullscreen mode

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:

npm install nativewind && npm install -D tailwindcss@3.3.2
Enter fullscreen mode Exit fullscreen mode

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:

npx tailwindcss init 
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
+   plugins: ["nativewind/babel"],
  };
};
Enter fullscreen mode Exit fullscreen mode

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:

Image description

Image description

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:

// 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: [],
}
Enter fullscreen mode Exit fullscreen mode

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 has two columns. 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:

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

  • data: The actual data that will be iterated through and rendered
  • renderItem: A function that should return a JSX element representing the view of each iteration. The function takes each item from data as a parameter
  • keyExtractor: Used to specify a unique key for each item in the list, helping React to efficiently identify and update list items. It also takes each item in data as a prop

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

// 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
        }}
      />
      <Navbar />
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

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: User Interface With Borders Around Items 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:

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now here’s what we have: Evenly Distributed Borders On Our FlatList User Interface 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:

// 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 h-56 w-full mx-auto object-cover bg-slate-500 rounded-lg'
        source={image_url}
      />
      <Text className='text-dark mb-3'>
        {name.substring(0, 30) + '...'}
      </Text>
      <Text className='text-dark dark:text-white font-bold'>{`${price}.00`}</Text>
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

npm i react-native-heroicons react-native-svg
Enter fullscreen mode Exit fullscreen mode

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:

// 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='px-8 py-6 bg-white shadow-top flex-row items-center justify-between'
    >
      <HomeIcon color="black" size={28} />
      <HeartIcon color="black" size={28} />
      <ShoppingCartIcon color="black" size={28} />
      <Pressable>
        <MoonIcon color="black" size={28} />
      </Pressable>
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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, and flatlist */}
      <Navbar />
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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: [],
}
Enter fullscreen mode Exit fullscreen mode

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

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>
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

User Interface With Navbar Component 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:

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 (
    {/* ... */}
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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='px-8 py-6 bg-white shadow-top dark:bg-soft-dark flex-row items-center justify-between'
    >
      <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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

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

// 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 h-56 w-full mx-auto object-cover bg-slate-500 rounded-lg'
        source={image_url}
      />
      <Text className='text-dark dark:text-white mb-3'>
        {name.substring(0, 30) + '...'}
      </Text>
      <Text className='text-dark dark:text-white font-bold'>{`${price}.00`}</Text>
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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='flex-[1] bg-white dark:bg-dark 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 the dependency on styledComponents and improves compatibility with the Tailwind framework used on the web.

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:

  • Streamlined installation: NativeWind v4 is expected to introduce a more efficient installation process, integrating react-reanimated as a peer dependency
  • Bug fixes and compatibility: With NativeWind v4, efforts are likely underway to address any breaking changes introduced by recent updates to Tailwind CSS, ensuring smoother integration and consistent functionality
  • New APIs: There is an introduction of fresh APIs like vars() and withNativewind(), offering enhanced control over styling workflows within React Native applications

Conclusion

NativeWind is doing a great job serving as a bridge for writing Tailwind CSS in 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.


LogRocket: Instantly recreate issues in your React Native apps

LogRocket Signup

LogRocketis a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — try LogRocket for free.

Top comments (1)

Collapse
 
loyaldev profile image
k. Fidèle Eklou

Thanks !