Back to Guide

Thursday, June 12, 2025

React Native with EXPO Guide

cover

React Native and Native Wind Setup

1. Create a new expo project using pnpm

# Create a new Expo project with TypeScript template
pnpm create expo-app my-grocery-app
cd my-grocery-app

# Start the development server
pnpm start

2. Install NativeWind via their docs

Step 2.1: Install NativeWind and dependencies

# Install NativeWind and required dependencies
pnpm install nativewind react-native-reanimated@~3.17.4 react-native-safe-area-context@5.4.0
pnpm install -D tailwindcss@^3.4.17 prettier-plugin-tailwindcss@^0.5.11

Step 2.2: Initialize Tailwind CSS

# Initialize Tailwind config
npx tailwindcss init

Step 2.3: Configure tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./App.{js,jsx,ts,tsx}",
    "./app/**/*.{js,jsx,ts,tsx}",
    "./components/**/*.{js,jsx,ts,tsx}",
  ],
  presets: [require("nativewind/preset")],
  theme: {
    extend: {},
  },
  plugins: [],
};

Step 2.4: Add Babel preset (babel.config.js)

module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      ["babel-preset-expo", { jsxImportSource: "nativewind" }],
      "nativewind/babel",
    ],
  };
};

Step 2.5: Configure Metro (metro.config.js)

Create a metro.config.js file in the root of your project if you don't already have one, then add the following configuration:

const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");

const config = getDefaultConfig(__dirname);

module.exports = withNativeWind(config, { input: "./global.css" });

Step 2.6: Create global.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Step 2.6: Update app/_layout.tsx

import "../global.css";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";

// Prevent the splash screen from auto-hiding
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const [loaded] = useFonts({
    SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
  });

  useEffect(() => {
    if (loaded) {
      SplashScreen.hideAsync();
    }
  }, [loaded]);

  if (!loaded) {
    return null;
  }

  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
    </Stack>
  );
}

TypeScript setup (optional)

If you're using TypeScript in your project, you'll need to set up the type definitions. Nativewind extends the React Native types via declaration merging. The simplest method to include the types is to create a new nativewind-env.d.ts file and add a triple-slash directive referencing the types.

/// <reference types="nativewind/types" />

3. Create the pages

Step 3.1: Create page structure in app/(tabs)/

Create these files in your app/(tabs)/ directory:

app/(tabs)/
├── index.tsx        (Home)
├── browse.tsx       (Browse)
├── deals.tsx        (Deals)
├── cart.tsx         (Cart)
├── account.tsx      (Account)
└── explore.tsx      (Explore - if needed)

Step 3.2: Home page (app/(tabs)/index.tsx)

import React from "react";
import { View, Text, ScrollView, SafeAreaView } from "react-native";

export default function HomeScreen() {
  return (
    <SafeAreaView className="flex-1 bg-white">
      <ScrollView className="flex-1 px-4">
        <View className="mt-5 mb-8">
          <Text className="text-2xl font-bold text-gray-900 mb-2">
            Welcome back! 👋
          </Text>
          <Text className="text-base text-gray-600">
            Discover fresh arrivals and great deals
          </Text>
        </View>

        {/* Add your home content here */}
        <View className="bg-gray-50 rounded-2xl p-4 mb-4">
          <Text className="text-lg font-semibold text-gray-900">
            New Arrivals
          </Text>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}

Step 3.3: Browse page (app/(tabs)/browse.tsx)

import React from "react";
import { View, Text, SafeAreaView } from "react-native";

export default function BrowseScreen() {
  return (
    <SafeAreaView className="flex-1 bg-white">
      <View className="flex-1 px-4 pt-5">
        <Text className="text-2xl font-bold text-gray-900 mb-4">
          Browse Categories
        </Text>

        {/* Add your browse content here */}
        <View className="bg-gray-50 rounded-2xl p-4">
          <Text className="text-lg font-semibold text-gray-900">
            Search and browse products
          </Text>
        </View>
      </View>
    </SafeAreaView>
  );
}

Step 3.4: Deals page (app/(tabs)/deals.tsx)

import React from "react";
import { View, Text, SafeAreaView } from "react-native";

export default function DealsScreen() {
  return (
    <SafeAreaView className="flex-1 bg-white">
      <View className="flex-1 px-4 pt-5">
        <Text className="text-2xl font-bold text-gray-900 mb-4">
          Special Deals 🏷️
        </Text>

        {/* Add your deals content here */}
        <View className="bg-green-50 rounded-2xl p-4">
          <Text className="text-lg font-semibold text-green-800">
            Today's best offers
          </Text>
        </View>
      </View>
    </SafeAreaView>
  );
}

Step 3.5: Cart page (app/(tabs)/cart.tsx)

import React from "react";
import { View, Text, SafeAreaView } from "react-native";

export default function CartScreen() {
  return (
    <SafeAreaView className="flex-1 bg-white">
      <View className="flex-1 px-4 pt-5">
        <Text className="text-2xl font-bold text-gray-900 mb-4">
          Shopping Cart 🛒
        </Text>

        {/* Add your cart content here */}
        <View className="bg-gray-50 rounded-2xl p-4">
          <Text className="text-lg font-semibold text-gray-900">
            Your cart is empty
          </Text>
        </View>
      </View>
    </SafeAreaView>
  );
}

Step 3.6: Account page (app/(tabs)/account.tsx)

import React from "react";
import { View, Text, SafeAreaView } from "react-native";

export default function AccountScreen() {
  return (
    <SafeAreaView className="flex-1 bg-white">
      <View className="flex-1 px-4 pt-5">
        <Text className="text-2xl font-bold text-gray-900 mb-4">
          My Account 👤
        </Text>

        {/* Add your account content here */}
        <View className="bg-gray-50 rounded-2xl p-4">
          <Text className="text-lg font-semibold text-gray-900">
            Profile settings and preferences
          </Text>
        </View>
      </View>
    </SafeAreaView>
  );
}

4. Create Colors object and Update with your colors

Step 4.1: Create constants/Colors.ts

/**
 * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
 */

const tintColorLight = "#067661";
const tintColorDark = "#00B050";

export const Colors = {
  light: {
    text: "#11181C",
    background: "#FFFFFF",
    tint: tintColorLight,
    icon: "#687076",
    tabIconDefault: "#9CA3AF",
    tabIconSelected: "#165e51",
    tabBarBackground: "#FFFFFF",
    tabBarBorder: "#F3F4F6",
    // Additional grocery app colors
    primary: "#067661",
    primaryDark: "#165e51",
    secondary: "#00B050",
    inactive: "#9CA3AF",
    success: "#10B981",
    warning: "#F59E0B",
    error: "#EF4444",
    cardBackground: "#F9FAFB",
    border: "#E5E7EB",
  },
  dark: {
    text: "#ECEDEE",
    background: "#151718",
    tint: tintColorDark,
    icon: "#9BA1A6",
    tabIconDefault: "#6B7280",
    tabIconSelected: "#00B050",
    tabBarBackground: "#1F2937",
    tabBarBorder: "#374151",
    // Additional grocery app colors
    primary: "#00B050",
    primaryDark: "#059669",
    secondary: "#067661",
    inactive: "#6B7280",
    success: "#10B981",
    warning: "#F59E0B",
    error: "#EF4444",
    cardBackground: "#374151",
    border: "#4B5563",
  },
};

Step 4.2: Update tailwind.config.js with custom colors

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./App.{js,jsx,ts,tsx}",
    "./app/**/*.{js,jsx,ts,tsx}",
    "./components/**/*.{js,jsx,ts,tsx}",
  ],
  presets: [require("nativewind/preset")],
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: "#067661",
          dark: "#165e51",
        },
        secondary: "#00B050",
        grocery: {
          green: "#067661",
          lightGreen: "#00B050",
          gray: "#9CA3AF",
          lightGray: "#F9FAFB",
        },
      },
    },
  },
  plugins: [],
};

5. Create the Navigation tabs

Step 5.1: Update IconSymbol component (components/ui/IconSymbol.tsx)

// IconSymbol component using Material Icons from Expo Vector Icons

import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { SymbolWeight } from "expo-symbols";
import { ComponentProps } from "react";
import { OpaqueColorValue, type StyleProp, type TextStyle } from "react-native";

type IconMapping = Record<string, ComponentProps<typeof MaterialIcons>["name"]>;
type IconSymbolName = keyof typeof MAPPING;

/**
 * Add your SF Symbols to Material Icons mappings here.
 * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
 * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
 */
const MAPPING = {
  // Navigation icons
  "house.fill": "home",
  "paperplane.fill": "send",
  magnifyingglass: "search",
  "tag.fill": "local-offer",
  "cart.fill": "shopping-cart",
  "person.fill": "person",
  "explore.fill": "explore",

  // UI and utility icons
  "square.grid.2x2": "grid-view",
  "heart.fill": "favorite",
  heart: "favorite-border",
  plus: "add",
  minus: "remove",
  "star.fill": "star",
  star: "star-border",
  "chevron.right": "chevron-right",
  "chevron.left": "chevron-left",
  "chevron.left.forwardslash.chevron.right": "code",

  // Common app icons
  "location.fill": "location-on",
  location: "location-on",
  "clock.fill": "access-time",
  "bell.fill": "notifications",
  bell: "notifications-none",
  "gearshape.fill": "settings",
  "line.horizontal.3": "menu",
  xmark: "close",
  "arrow.left": "arrow-back",
  "arrow.right": "arrow-forward",
  "arrow.up": "arrow-upward",
  "arrow.down": "arrow-downward",

  // Action icons
  "eye.fill": "visibility",
  "eye.slash.fill": "visibility-off",
  checkmark: "check",
  "exclamationmark.circle.fill": "error",
  "info.circle.fill": "info",
  "trash.fill": "delete",
  pencil: "edit",
  "camera.fill": "camera-alt",
  "photo.fill": "photo",

  // Filter and sort
  "line.horizontal.3.decrease.circle": "filter-list",
  "arrow.up.arrow.down": "sort",

  // Transfer and sharing
  "square.and.arrow.up": "share",
  "bookmark.fill": "bookmark",
  bookmark: "bookmark-border",
  "arrow.clockwise": "refresh",

  // Communication icons
  "phone.fill": "phone",
  "envelope.fill": "email",
  "message.circle.fill": "chat",

  // Shopping specific
  "bag.fill": "shopping-bag",
  "creditcard.fill": "credit-card",
  percent: "percent",
  "dollarsign.circle.fill": "attach-money",

  // Categories
  "leaf.fill": "eco",
  "flame.fill": "whatshot",
  snowflake: "ac-unit",
  "drop.fill": "water-drop",

  // Additional utility
  "questionmark.circle.fill": "help",
  "plus.circle.fill": "add-circle",
  "minus.circle.fill": "remove-circle",
  "play.fill": "play-arrow",
  "pause.fill": "pause",
  "stop.fill": "stop",
} as IconMapping;

/**
 * An icon component that uses Material Icons from Expo Vector Icons.
 * This ensures a consistent look across platforms with great icon coverage.
 * Icon `name`s are based on SF Symbols and mapped to Material Icons.
 */
export function IconSymbol({
  name,
  size = 24,
  color,
  style,
  weight,
}: {
  name: IconSymbolName;
  size?: number;
  color: string | OpaqueColorValue;
  style?: StyleProp<TextStyle>;
  weight?: SymbolWeight;
}) {
  const materialIconName = MAPPING[name];

  if (!materialIconName) {
    console.warn(`IconSymbol: Icon "${name}" not found in mapping`);
    // Return a fallback icon
    return (
      <MaterialIcons
        color={color}
        size={size}
        name="help-outline"
        style={style}
      />
    );
  }

  return (
    <MaterialIcons
      color={color}
      size={size}
      name={materialIconName}
      style={style}
    />
  );
}

// Export the available icon names for TypeScript support
export type IconName = IconSymbolName;

Step 5.2: Update Tab Layout (app/(tabs)/_layout.tsx)

import { Tabs } from "expo-router";
import React from "react";
import { Platform } from "react-native";

import { HapticTab } from "@/components/HapticTab";
import { IconSymbol } from "@/components/ui/IconSymbol";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { Colors } from "@/constants/Colors";
import { useColorScheme } from "@/hooks/useColorScheme";

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[colorScheme ?? "light"].secondary,
        tabBarInactiveTintColor: Colors[colorScheme ?? "light"].tabIconDefault,
        headerShown: false,
        tabBarButton: HapticTab,
        tabBarBackground: TabBarBackground,
        tabBarStyle: {
          borderTopWidth: 0.5,
          borderTopColor: Colors[colorScheme ?? "light"].tabBarBorder,
          height: Platform.OS === "ios" ? 88 : 120,
          paddingBottom: Platform.OS === "ios" ? 25 : 12,
          paddingTop: 8,
          paddingHorizontal: 16,
          shadowColor: "#000",
          shadowOffset: {
            width: 0,
            height: -2,
          },
          shadowOpacity: 0.05,
          shadowRadius: 8,
          elevation: 8,
          ...Platform.select({
            ios: {
              position: "absolute",
              backgroundColor: `${
                Colors[colorScheme ?? "light"].tabBarBackground
              }F8`, // 97% opacity
              backdropFilter: "blur(20px)",
            },
            default: {
              backgroundColor: Colors[colorScheme ?? "light"].tabBarBackground,
            },
          }),
        },
        tabBarLabelStyle: {
          fontSize: 11,
          fontWeight: "500",
          marginTop: 4,
          fontFamily: Platform.OS === "ios" ? "System" : "Roboto",
        },
        tabBarIconStyle: {
          marginTop: 2,
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol
              size={22}
              name="house.fill"
              color={
                focused
                  ? Colors[colorScheme ?? "light"].tabIconSelected
                  : Colors[colorScheme ?? "light"].tabIconDefault
              }
            />
          ),
        }}
      />
      <Tabs.Screen
        name="browse"
        options={{
          title: "Browse",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol
              size={22}
              name="magnifyingglass"
              color={
                focused
                  ? Colors[colorScheme ?? "light"].tabIconSelected
                  : Colors[colorScheme ?? "light"].tabIconDefault
              }
            />
          ),
        }}
      />
      <Tabs.Screen
        name="deals"
        options={{
          title: "Deal",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol
              size={22}
              name="tag.fill"
              color={
                focused
                  ? Colors[colorScheme ?? "light"].tabIconSelected
                  : Colors[colorScheme ?? "light"].tabIconDefault
              }
            />
          ),
        }}
      />
      <Tabs.Screen
        name="cart"
        options={{
          title: "Cart",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol
              size={22}
              name="cart.fill"
              color={
                focused
                  ? Colors[colorScheme ?? "light"].tabIconSelected
                  : Colors[colorScheme ?? "light"].tabIconDefault
              }
            />
          ),
        }}
      />
      <Tabs.Screen
        name="account"
        options={{
          title: "Account",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol
              size={22}
              name="person.fill"
              color={
                focused
                  ? Colors[colorScheme ?? "light"].tabIconSelected
                  : Colors[colorScheme ?? "light"].tabIconDefault
              }
            />
          ),
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: "Explore",
          // href: null,
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol
              size={22}
              name="explore.fill"
              color={
                focused
                  ? Colors[colorScheme ?? "light"].tabIconSelected
                  : Colors[colorScheme ?? "light"].tabIconDefault
              }
            />
          ),
        }}
      />
    </Tabs>
  );
}

6. Additional Setup Steps

Step 6.1: Configure TypeScript for NativeWind

Create or update nativewind-env.d.ts:

/// <reference types="nativewind/types" />

Step 6.2: Update app.json

  • Switch to metro
{
  "expo": {
    "web": {
      "bundler": "metro"
    }
  }
}

Step 6.3: Test your setup

# Clear cache and restart
pnpm start --clear

# Or for specific platforms
pnpm run ios
pnpm run android
pnpm run web

7. Common Issues and Solutions

Issue 1: NativeWind styles not applying

  • Make sure global.css is imported in _layout.tsx
  • Verify Metro config includes NativeWind
  • Clear Metro cache: pnpm start --clear

Issue 2: TypeScript errors

  • Ensure nativewind-env.d.ts is created
  • Update tsconfig.json includes

Issue 3: Navigation not working

  • Check that all page files exist in app/(tabs)/
  • Verify icon mappings in IconSymbol component

8. Next Steps

  1. Add components: Create reusable UI components
  2. State management: Add Zustand or Redux for global state
  3. API integration: Connect to backend services
  4. Testing: Add Jest and React Native Testing Library
  5. Build optimization: Configure for production builds

Useful Commands

# Development
pnpm start
pnpm run ios
pnpm run android
pnpm run web

# Building
pnpm run build
expo build

# Useful tools
npx expo install --fix
npx expo doctor

Subscribe to My Latest Guides

All the latest Guides and tutorials, straight from the team.

Subscribe to be the first to receive the new Guide when it comes out

Get All Guides at Once