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',
},
})
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 .
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
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
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
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: [],
}
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"],
};
};
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:
// 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 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>
)
}
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 fromdata
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 indata
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>
)
}
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: 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:
- Grab the current device's width
- Divide its value by the number of columns (2)
- 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>
)
}
Now here’s what we have: 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>
)
}
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
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>
)
}
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>
)
}
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: [],
}
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>
}
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>
)
}
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 (
{/* ... */}
);
}
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>
)
}
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>
)
}
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>
)
}
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:
- Native-specific styling: In RN, some styles on particular elements can only be applied through a style object. Take a
FlatList
for example — thecolumnWrapperStyle
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 nativeStylesheet
object so embrace mixing your NativeWind with thestyle
prop - 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 styledComponent
s 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()
andwithNativewind()
, 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
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 (2)
Not working followed every step here 😔
Thanks !