reactnative
/

React Native Tab Navigator – Bottom Tabs, Top Tabs & Custom Tab Bars

Last Sync: Today

On this page

11
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

reactnative

React Native Tab Navigator – Bottom Tabs, Top Tabs & Custom Tab Bars

Tab Navigator – Organizing App Screens

Tab navigators are essential for organizing app content into logical categories. React Navigation provides two primary tab implementations: @react-navigation/bottom-tabs (standard iOS/Android bottom tab bar) and @react-navigation/material-top-tabs (Material Design top tabs for Android). In 2026, React Navigation 8 introduces native bottom tabs by default, featuring iOS 26 liquid glass effects and improved performance.

  1. Installation & Setup

Install the required packages for bottom tabs and optional material top tabs. For React Navigation 8, native bottom tabs are the default implementation.

BASHRead-only
1
# Bottom Tabs (React Navigation 7/8)
npm install @react-navigation/bottom-tabs

# Material Top Tabs (Android-style)
npm install @react-navigation/material-top-tabs react-native-tab-view

# For React Navigation 8 - Native bottom tabs (default, no extra package needed)
# Built into @react-navigation/native

# Peer dependencies (Expo managed)
npx expo install react-native-screens react-native-safe-area-context

# For Material Top Tabs (bare React Native)
npm install react-native-pager-view
cd ios && pod install && cd ..

# Icons (optional, for tab bar icons)
npm install react-native-vector-icons
# or for Expo: npx expo install @expo/vector-icons

  1. Bottom Tab Navigator (Standard)

Bottom tabs are the most common tab pattern in mobile apps. They appear at the bottom of the screen and are always visible. React Navigation 8 uses native bottom tabs by default for iOS 26 liquid glass effects.

React TSXRead-only
1
// BottomTabNavigator.tsx
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { View, Text, StyleSheet, Platform } from 'react-native';

// Screen components
function HomeScreen() {
  return (
    <View style={styles.screen}>
      <Text style={styles.title}>Home Screen</Text>
    </View>
  );
}

function SearchScreen() {
  return (
    <View style={styles.screen}>
      <Text style={styles.title}>Search Screen</Text>
    </View>
  );
}

function NotificationsScreen() {
  return (
    <View style={styles.screen}>
      <Text style={styles.title}>Notifications</Text>
    </View>
  );
}

function ProfileScreen() {
  return (
    <View style={styles.screen}>
      <Text style={styles.title}>Profile Screen</Text>
    </View>
  );
}

// Type definitions
type TabParamList = {
  Home: undefined;
  Search: undefined;
  Notifications: undefined;
  Profile: undefined;
};

const Tab = createBottomTabNavigator<TabParamList>();

export default function BottomTabNavigator() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName: keyof typeof Ionicons.glyphMap = 'home-outline';

            if (route.name === 'Home') {
              iconName = focused ? 'home' : 'home-outline';
            } else if (route.name === 'Search') {
              iconName = focused ? 'search' : 'search-outline';
            } else if (route.name === 'Notifications') {
              iconName = focused ? 'notifications' : 'notifications-outline';
            } else if (route.name === 'Profile') {
              iconName = focused ? 'person' : 'person-outline';
            }

            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: '#007AFF',
          tabBarInactiveTintColor: 'gray',
          tabBarStyle: {
            paddingBottom: Platform.OS === 'ios' ? 20 : 10,
            height: Platform.OS === 'ios' ? 85 : 65,
            backgroundColor: '#fff',
          },
          tabBarLabelStyle: {
            fontSize: 12,
            fontWeight: '500',
          },
          headerStyle: {
            backgroundColor: '#007AFF',
          },
          headerTintColor: '#fff',
          headerTitleStyle: {
            fontWeight: 'bold',
          },
        })}
      >
        <Tab.Screen 
          name="Home" 
          component={HomeScreen} 
          options={{ title: 'Home' }}
        />
        <Tab.Screen 
          name="Search" 
          component={SearchScreen} 
          options={{ title: 'Discover' }}
        />
        <Tab.Screen 
          name="Notifications" 
          component={NotificationsScreen} 
          options={{ 
            title: 'Alerts',
            tabBarBadge: 3, // Show badge with number 3
          }}
        />
        <Tab.Screen 
          name="Profile" 
          component={ProfileScreen} 
          options={{ title: 'My Profile' }}
        />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#fff',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
  },
});

// React Navigation 8 - Native bottom tabs (simpler syntax)
// In React Navigation 8, native bottom tabs are the default:
// import { createBottomTabNavigator } from '@react-navigation/native';
// No need for @react-navigation/bottom-tabs package

  1. Material Top Tab Navigator

Material Top Tabs follow Material Design guidelines and are commonly used on Android. They appear at the top of the screen and can scroll horizontally.

React TSXRead-only
1
// MaterialTopTabNavigator.tsx
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import { View, Text, StyleSheet } from 'react-native';

// Screen components
function FeedScreen() {
  return (
    <View style={styles.screen}>
      <Text>Feed - Latest posts from people you follow</Text>
    </View>
  );
}

function TrendingScreen() {
  return (
    <View style={styles.screen}>
      <Text>Trending - Popular content right now</Text>
    </View>
  );
}

function LatestScreen() {
  return (
    <View style={styles.screen}>
      <Text>Latest - Newest content first</Text>
    </View>
  );
}

function FavoritesScreen() {
  return (
    <View style={styles.screen}>
      <Text>Favorites - Content you've saved</Text>
    </View>
  );
}

const Tab = createMaterialTopTabNavigator();

export default function MaterialTopTabNavigator() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={{
          tabBarLabelStyle: { fontSize: 14, fontWeight: '600', textTransform: 'none' },
          tabBarIndicatorStyle: { backgroundColor: '#007AFF', height: 3 },
          tabBarActiveTintColor: '#007AFF',
          tabBarInactiveTintColor: 'gray',
          tabBarStyle: { backgroundColor: '#fff', elevation: 4, shadowOpacity: 0.1 },
          // Scrollable tabs for many items
          tabBarScrollEnabled: true,
          tabBarItemStyle: { width: 'auto', minWidth: 80 },
        }}
      >
        <Tab.Screen name="Feed" component={FeedScreen} />
        <Tab.Screen name="Trending" component={TrendingScreen} />
        <Tab.Screen name="Latest" component={LatestScreen} />
        <Tab.Screen name="Favorites" component={FavoritesScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 16,
  },
});

// Material Top Tabs with custom indicator
function CustomIndicatorScreen() {
  return (
    <Tab.Navigator
      screenOptions={{
        tabBarIndicator: () => (
          <View
            style={{
              backgroundColor: '#007AFF',
              height: 4,
              borderRadius: 2,
              width: '50%',
              alignSelf: 'center',
            }}
          />
        ),
        tabBarStyle: { elevation: 0, shadowOpacity: 0 },
      }}
    >
      {/* Tabs */}
    </Tab.Navigator>
  );
}

  1. Custom Tab Bar Component

Create fully customized tab bars with animations, custom buttons, and unique styling for brand-specific designs.

React TSXRead-only
1
// CustomTabBar.tsx
import * as React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';

// Custom tab bar component
function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
  const [isTabBarVisible, setIsTabBarVisible] = React.useState(true);

  // Optional: Hide tab bar on scroll
  // You can implement scroll listener to hide/show tab bar

  return (
    <View style={[styles.tabBar, !isTabBarVisible && styles.tabBarHidden]}>
      {state.routes.map((route, index) => {
        const { options } = descriptors[route.key];
        const label =
          options.tabBarLabel !== undefined
            ? options.tabBarLabel
            : options.title !== undefined
            ? options.title
            : route.name;

        const isFocused = state.index === index;

        const onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key,
            canPreventDefault: true,
          });

          if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name);
          }
        };

        const onLongPress = () => {
          navigation.emit({
            type: 'tabLongPress',
            target: route.key,
          });
        };

        // Get icon name
        let iconName: keyof typeof Ionicons.glyphMap = 'home-outline';
        if (route.name === 'Home') iconName = isFocused ? 'home' : 'home-outline';
        else if (route.name === 'Search') iconName = isFocused ? 'search' : 'search-outline';
        else if (route.name === 'Notifications') iconName = isFocused ? 'notifications' : 'notifications-outline';
        else if (route.name === 'Profile') iconName = isFocused ? 'person' : 'person-outline';

        // Special tab: Center button (for actions like camera, create post)
        if (route.name === 'Create') {
          return (
            <TouchableOpacity
              key={index}
              onPress={onPress}
              onLongPress={onLongPress}
              style={styles.centerButton}
              activeOpacity={0.8}
            >
              <View style={styles.centerButtonInner}>
                <Ionicons name="add" size={32} color="#fff" />
              </View>
            </TouchableOpacity>
          );
        }

        return (
          <TouchableOpacity
            key={index}
            accessibilityRole="button"
            accessibilityState={isFocused ? { selected: true } : {}}
            accessibilityLabel={options.tabBarAccessibilityLabel}
            onPress={onPress}
            onLongPress={onLongPress}
            style={styles.tabItem}
          >
            <View style={styles.tabItemInner}>
              <Ionicons name={iconName} size={24} color={isFocused ? '#007AFF' : 'gray'} />
              <Text style={[styles.tabLabel, { color: isFocused ? '#007AFF' : 'gray' }]}>
                {typeof label === 'string' ? label : route.name}
              </Text>
              {/* Badge example */}
              {route.name === 'Notifications' && (
                <View style={styles.badge}>
                  <Text style={styles.badgeText}>3</Text>
                </View>
              )}
            </View>
          </TouchableOpacity>
        );
      })}
    </View>
  );
}

// Using custom tab bar in navigator
function CustomTabBarExample() {
  return (
    <Tab.Navigator
      tabBar={(props) => <CustomTabBar {...props} />}
      screenOptions={{ headerShown: true }}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Search" component={SearchScreen} />
      <Tab.Screen name="Create" component={CreateScreen} />
      <Tab.Screen name="Notifications" component={NotificationsScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

const styles = StyleSheet.create({
  tabBar: {
    flexDirection: 'row',
    backgroundColor: '#fff',
    paddingBottom: Platform.OS === 'ios' ? 20 : 10,
    paddingTop: 8,
    borderTopWidth: 1,
    borderTopColor: '#e0e0e0',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: -2 },
    shadowOpacity: 0.05,
    shadowRadius: 3,
    elevation: 8,
  },
  tabBarHidden: {
    transform: [{ translateY: 100 }],
  },
  tabItem: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  tabItemInner: {
    alignItems: 'center',
    position: 'relative',
  },
  tabLabel: {
    fontSize: 11,
    marginTop: 4,
  },
  centerButton: {
    top: -20,
    justifyContent: 'center',
    alignItems: 'center',
    marginHorizontal: 8,
  },
  centerButtonInner: {
    width: 56,
    height: 56,
    borderRadius: 28,
    backgroundColor: '#007AFF',
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#007AFF',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 4,
    elevation: 5,
  },
  badge: {
    position: 'absolute',
    top: -8,
    right: -12,
    backgroundColor: '#FF3B30',
    borderRadius: 10,
    minWidth: 18,
    height: 18,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 4,
  },
  badgeText: {
    color: '#fff',
    fontSize: 10,
    fontWeight: 'bold',
  },
});

  1. Tab Badges & Dynamic Updates

Show badges on tab icons to indicate new content, pending actions, or notification counts. Update them dynamically based on app state.

React TSXRead-only
1
// TabBadges.tsx
import * as React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, Text, Button, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

const Tab = createBottomTabNavigator();

function HomeScreen({ navigation }: { navigation: any }) {
  return (
    <View style={styles.container}>
      <Text>Home Screen</Text>
      <Button
        title="Clear Notifications Badge"
        onPress={() => navigation.setOptions({ tabBarBadge: null })}
      />
    </View>
  );
}

function NotificationsScreen({ navigation }: { navigation: any }) {
  const [count, setCount] = React.useState(0);

  const addNotification = () => {
    const newCount = count + 1;
    setCount(newCount);
    // Update badge from within the screen
    navigation.setOptions({ tabBarBadge: newCount > 0 ? newCount : null });
    
    // Also update parent tab navigator if needed
    navigation.getParent()?.setOptions({
      tabBarBadge: newCount > 0 ? newCount : null,
    });
  };

  const clearAll = () => {
    setCount(0);
    navigation.setOptions({ tabBarBadge: null });
  };

  return (
    <View style={styles.container}>
      <Text>Notifications: {count}</Text>
      <Button title="Add Notification" onPress={addNotification} />
      <Button title="Clear All" onPress={clearAll} />
    </View>
  );
}

// Dynamic badge based on state (using Redux/Zustand)
function DynamicBadgeTab() {
  const notificationCount = useNotificationStore((state) => state.unreadCount);

  return (
    <Tab.Navigator>
      <Tab.Screen
        name="Home"
        component={HomeScreen}
        options={{
          tabBarBadge: undefined,
        }}
      />
      <Tab.Screen
        name="Notifications"
        component={NotificationsScreen}
        options={{
          tabBarBadge: notificationCount > 0 ? notificationCount : undefined,
          tabBarBadgeStyle: {
            backgroundColor: '#FF3B30',
            color: '#fff',
            fontSize: 11,
          },
        }}
      />
    </Tab.Navigator>
  );
}

// Animated badge (simple pulse animation)
function AnimatedBadge({ count }: { count: number }) {
  const pulseAnim = React.useRef(new Animated.Value(1)).current;

  React.useEffect(() => {
    if (count > 0) {
      Animated.sequence([
        Animated.timing(pulseAnim, {
          toValue: 1.3,
          duration: 200,
          useNativeDriver: true,
        }),
        Animated.timing(pulseAnim, {
          toValue: 1,
          duration: 200,
          useNativeDriver: true,
        }),
      ]).start();
    }
  }, [count]);

  if (count === 0) return null;

  return (
    <Animated.View
      style={[
        styles.animatedBadge,
        { transform: [{ scale: pulseAnim }] },
      ]}
    >
      <Text style={styles.badgeText}>{count > 99 ? '99+' : count}</Text>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 16 },
  animatedBadge: {
    position: 'absolute',
    top: -5,
    right: -10,
    backgroundColor: '#FF3B30',
    borderRadius: 12,
    minWidth: 20,
    height: 20,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 4,
  },
  badgeText: { color: '#fff', fontSize: 11, fontWeight: 'bold' },
});

  1. Nested Tab Navigators

Combine tab navigators with stack navigators for complex navigation patterns. Tabs can contain stacks, and stacks can contain tabs.

React TSXRead-only
1
// NestedTabNavigators.tsx
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Button, View, Text, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

// ----- Home Stack (nested inside Home Tab) -----
type HomeStackParamList = {
  HomeList: undefined;
  HomeDetail: { id: number };
};

const HomeStack = createNativeStackNavigator<HomeStackParamList>();

function HomeListScreen({ navigation }: { navigation: any }) {
  return (
    <View style={styles.screen}>
      <Text>Home List Screen</Text>
      <Button
        title="Go to Detail"
        onPress={() => navigation.navigate('HomeDetail', { id: 42 })}
      />
    </View>
  );
}

function HomeDetailScreen({ route, navigation }: { route: any; navigation: any }) {
  const { id } = route.params;
  return (
    <View style={styles.screen}>
      <Text>Home Detail - ID: {id}</Text>
      <Button title="Go Back" onPress={() => navigation.goBack()} />
      <Button
        title="Go to Profile Tab"
        onPress={() => navigation.getParent()?.navigate('ProfileTab')}
      />
    </View>
  );
}

function HomeStackScreen() {
  return (
    <HomeStack.Navigator>
      <HomeStack.Screen name="HomeList" component={HomeListScreen} />
      <HomeStack.Screen name="HomeDetail" component={HomeDetailScreen} />
    </HomeStack.Navigator>
  );
}

// ----- Profile Stack (nested inside Profile Tab) -----
type ProfileStackParamList = {
  ProfileMain: undefined;
  ProfileEdit: undefined;
};

const ProfileStack = createNativeStackNavigator<ProfileStackParamList>();

function ProfileMainScreen({ navigation }: { navigation: any }) {
  return (
    <View style={styles.screen}>
      <Text>Profile Main</Text>
      <Button title="Edit Profile" onPress={() => navigation.navigate('ProfileEdit')} />
    </View>
  );
}

function ProfileEditScreen({ navigation }: { navigation: any }) {
  return (
    <View style={styles.screen}>
      <Text>Edit Profile</Text>
      <Button title="Save" onPress={() => navigation.goBack()} />
    </View>
  );
}

function ProfileStackScreen() {
  return (
    <ProfileStack.Navigator>
      <ProfileStack.Screen name="ProfileMain" component={ProfileMainScreen} />
      <ProfileStack.Screen name="ProfileEdit" component={ProfileEditScreen} />
    </ProfileStack.Navigator>
  );
}

// ----- Settings Screen (simple) -----
function SettingsScreen() {
  return (
    <View style={styles.screen}>
      <Text>Settings Screen</Text>
    </View>
  );
}

// ----- Main Tab Navigator -----
type TabParamList = {
  HomeTab: undefined;
  SettingsTab: undefined;
  ProfileTab: undefined;
};

const Tab = createBottomTabNavigator<TabParamList>();

function MainTabNavigator() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          let iconName: keyof typeof Ionicons.glyphMap = 'home-outline';
          if (route.name === 'HomeTab') iconName = focused ? 'home' : 'home-outline';
          else if (route.name === 'SettingsTab') iconName = focused ? 'settings' : 'settings-outline';
          else if (route.name === 'ProfileTab') iconName = focused ? 'person' : 'person-outline';
          return <Ionicons name={iconName} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#007AFF',
        headerShown: false, // Hide headers on tabs (stacks have their own)
      })}
    >
      <Tab.Screen name="HomeTab" component={HomeStackScreen} />
      <Tab.Screen name="SettingsTab" component={SettingsScreen} />
      <Tab.Screen name="ProfileTab" component={ProfileStackScreen} />
    </Tab.Navigator>
  );
}

export default function NestedTabsApp() {
  return (
    <NavigationContainer>
      <MainTabNavigator />
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 16 },
});

// Dynamically hiding tab bar on certain screens
import { getFocusedRouteNameFromRoute } from '@react-navigation/native';

function getTabBarVisibility(route: any) {
  const routeName = getFocusedRouteNameFromRoute(route) ?? 'HomeList';
  const hideOnScreens = ['HomeDetail', 'ProfileEdit'];
  return { display: hideOnScreens.includes(routeName) ? 'none' : 'flex' };
}

// Usage in tab navigator screen options:
// <Tab.Screen
//   name="HomeTab"
//   component={HomeStackScreen}
//   options={({ route }) => ({
//     tabBarStyle: getTabBarVisibility(route),
//   })}
// />

  1. TypeScript Integration

Full TypeScript support for tab navigators ensures type safety for route names and parameters.

React TSXRead-only
1
// TabNavigator.types.ts
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { CompositeScreenProps } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';

// Define param list for tabs
export type RootTabParamList = {
  Home: undefined;
  Search: { query?: string };
  Notifications: undefined;
  Profile: { userId: string };
};

// Define param list for stacks inside tabs
export type HomeStackParamList = {
  HomeList: undefined;
  HomeDetail: { id: number; title: string };
};

export type ProfileStackParamList = {
  ProfileMain: undefined;
  ProfileEdit: { readonly?: boolean };
};

// Composite screen props (tab + stack)
export type HomeDetailScreenProps = CompositeScreenProps<
  NativeStackScreenProps<HomeStackParamList, 'HomeDetail'>,
  BottomTabScreenProps<RootTabParamList>
>;

// TabNavigator.tsx with TypeScript
import * as React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { RootTabParamList } from './types';

const Tab = createBottomTabNavigator<RootTabParamList>();

export default function TabNavigator() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen 
        name="Search" 
        component={SearchScreen}
        initialParams={{ query: '' }}
      />
      <Tab.Screen name="Notifications" component={NotificationsScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

// Screen with typed navigation and route
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';

type SearchScreenProps = BottomTabScreenProps<RootTabParamList, 'Search'>;

function SearchScreen({ navigation, route }: SearchScreenProps) {
  const { query } = route.params;
  
  // TypeScript knows query is string | undefined
  return (
    <View>
      <Text>Search: {query || 'No query'}</Text>
      <Button
        title="Search for 'React'"
        onPress={() => navigation.setParams({ query: 'React' })}
      />
    </View>
  );
}

// Global type declaration for useNavigation
declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootTabParamList {}
  }
}

// Now useNavigation works without type arguments
function AnyScreen() {
  const navigation = useNavigation();
  // TypeScript knows all routes
  navigation.navigate('Profile', { userId: '123' });
  return null;
}

  1. Tab Navigator Performance

Optimize tab navigator performance with lazy loading, screen freezing, and proper memoization.

React TSXRead-only
1
// PerformanceOptimizations.tsx
import * as React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { ActivityIndicator, View } from 'react-native';

const Tab = createBottomTabNavigator();

// 1. Lazy loading screens (React.lazy)
const LazyProfileScreen = React.lazy(() => import('./ProfileScreen'));
const LazySettingsScreen = React.lazy(() => import('./SettingsScreen'));

function LazyLoadedScreen({ children }: { children: React.ReactNode }) {
  return (
    <React.Suspense fallback={<ActivityIndicator size="large" style={{ flex: 1 }} />}>
      {children}
    </React.Suspense>
  );
}

// 2. Memoized tab screens
const MemoizedHomeScreen = React.memo(function HomeScreen() {
  // Component logic
  return <View />;
});

// 3. Optimized tab navigator
function OptimizedTabNavigator() {
  return (
    <Tab.Navigator
      screenOptions={{
        // React Navigation 8 - Freeze inactive tabs
        freezeOnBlur: true,
        // Lazy load tabs (only render when focused first time)
        lazy: true,
        // Unmount inactive tabs (saves memory)
        unmountOnBlur: false, // Set to true for memory-constrained devices
      }}
    >
      <Tab.Screen 
        name="Home" 
        component={MemoizedHomeScreen}
        options={{ lazy: true }}
      />
      <Tab.Screen 
        name="Profile" 
        component={() => (
          <LazyLoadedScreen>
            <LazyProfileScreen />
          </LazyLoadedScreen>
        )}
      />
    </Tab.Navigator>
  );
}

// 4. Avoid re-renders with useCallback
function HomeScreenWithOptimization() {
  const handlePress = React.useCallback(() => {
    // Handle press
  }, []);

  return <Button title="Press" onPress={handlePress} />;
}

// 5. Tab bar visibility optimization
import { useFocusEffect } from '@react-navigation/native';

function ScreenThatHidesTabBar() {
  const navigation = useNavigation();

  React.useEffect(() => {
    const parent = navigation.getParent();
    parent?.setOptions({ tabBarStyle: { display: 'none' } });

    return () => {
      parent?.setOptions({ tabBarStyle: { display: 'flex' } });
    };
  }, [navigation]);

  return <View />;
}

// Performance Checklist:
// ✅ Use lazy: true for tabs not needed immediately
// ✅ Use freezeOnBlur: true (React Navigation 8)
// ✅ Memoize screen components with React.memo
// ✅ Lazy load heavy screens with React.lazy
// ✅ Avoid inline functions in tab options
// ✅ Use useCallback for event handlers
// ✅ Profile with React DevTools
// ✅ Consider unmountOnBlur for memory-constrained devices

  1. Advanced Tab Bar Animations

Create animated tab bars with spring animations, floating effects, and custom transitions.

React TSXRead-only
1
// AnimatedTabBar.tsx
import * as React from 'react';
import { View, TouchableOpacity, StyleSheet, Animated, Easing } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';

function AnimatedTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
  const [animatedValues] = React.useState(
    state.routes.map(() => new Animated.Value(0))
  );

  React.useEffect(() => {
    // Animate icons when tab changes
    state.routes.forEach((_, index) => {
      const isFocused = state.index === index;
      Animated.spring(animatedValues[index], {
        toValue: isFocused ? 1 : 0,
        useNativeDriver: true,
        friction: 6,
        tension: 40,
      }).start();
    });
  }, [state.index]);

  const onPress = (route: any, index: number) => {
    const event = navigation.emit({
      type: 'tabPress',
      target: route.key,
      canPreventDefault: true,
    });

    if (!event.defaultPrevented) {
      navigation.navigate(route.name);
    }
  };

  return (
    <View style={styles.container}>
      {state.routes.map((route, index) => {
        const isFocused = state.index === index;
        const scale = animatedValues[index].interpolate({
          inputRange: [0, 1],
          outputRange: [0.9, 1.2],
        });

        const translateY = animatedValues[index].interpolate({
          inputRange: [0, 1],
          outputRange: [0, -8],
        });

        const opacity = animatedValues[index].interpolate({
          inputRange: [0, 1],
          outputRange: [0.6, 1],
        });

        let iconName: keyof typeof Ionicons.glyphMap = 'home-outline';
        if (route.name === 'Home') iconName = isFocused ? 'home' : 'home-outline';
        else if (route.name === 'Search') iconName = isFocused ? 'search' : 'search-outline';
        else if (route.name === 'Profile') iconName = isFocused ? 'person' : 'person-outline';

        return (
          <TouchableOpacity
            key={index}
            onPress={() => onPress(route, index)}
            style={styles.tab}
          >
            <Animated.View
              style={[
                styles.iconContainer,
                {
                  transform: [{ scale }, { translateY }],
                },
              ]}
            >
              <Animated.View style={{ opacity }}>
                <Ionicons name={iconName} size={24} color={isFocused ? '#007AFF' : 'gray'} />
              </Animated.View>
            </Animated.View>
          </TouchableOpacity>
        );
      })}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    backgroundColor: '#fff',
    paddingVertical: 10,
    borderTopWidth: 1,
    borderTopColor: '#e0e0e0',
  },
  tab: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  iconContainer: {
    alignItems: 'center',
    justifyContent: 'center',
  },
});

// Floating tab bar effect
function FloatingTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
  const translateY = React.useRef(new Animated.Value(0)).current;

  React.useEffect(() => {
    // Animate tab bar on mount
    Animated.spring(translateY, {
      toValue: 0,
      useNativeDriver: true,
      friction: 8,
      tension: 40,
    }).start();
  }, []);

  return (
    <Animated.View
      style={[
        styles.floatingContainer,
        {
          transform: [{ translateY }],
        },
      ]}
    >
      <View style={styles.floatingTabBar}>
        {/* Tab buttons */}
      </View>
    </Animated.View>
  );
}

const floatingStyles = StyleSheet.create({
  floatingContainer: {
    position: 'absolute',
    bottom: 20,
    left: 20,
    right: 20,
  },
  floatingTabBar: {
    flexDirection: 'row',
    backgroundColor: '#fff',
    borderRadius: 30,
    paddingVertical: 8,
    paddingHorizontal: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 8,
  },
});

  1. Best Practices & Common Patterns

Production-proven patterns for tab navigator implementation.

React TSXRead-only
1
// TabNavigatorBestPractices.tsx

// 1. Centralized tab configuration
const TAB_CONFIG = {
  Home: {
    icon: { focused: 'home', unfocused: 'home-outline' },
    label: 'Home',
  },
  Search: {
    icon: { focused: 'search', unfocused: 'search-outline' },
    label: 'Discover',
  },
  Profile: {
    icon: { focused: 'person', unfocused: 'person-outline' },
    label: 'Profile',
  },
} as const;

// 2. Reusable tab bar icon component
function TabBarIcon({ 
  name, 
  focused, 
  color, 
  size 
}: { 
  name: keyof typeof TAB_CONFIG; 
  focused: boolean; 
  color: string; 
  size: number;
}) {
  const iconName = TAB_CONFIG[name].icon[focused ? 'focused' : 'unfocused'];
  return <Ionicons name={iconName} size={size} color={color} />;
}

// 3. Dynamic tab bar height for safe area
import { useSafeAreaInsets } from 'react-native-safe-area-context';

function SafeAreaTabBar(props: BottomTabBarProps) {
  const insets = useSafeAreaInsets();
  
  return (
    <View style={{ paddingBottom: insets.bottom }}>
      <BottomTabBar {...props} />
    </View>
  );
}

// 4. Tab press analytics tracking
function AnalyticsTabBar(props: BottomTabBarProps) {
  const { navigation, state } = props;
  
  const handleTabPress = (route: any, index: number) => {
    // Track tab press for analytics
    Analytics.trackEvent('tab_press', {
      tab_name: route.name,
      tab_index: index,
    });
    
    // Default navigation behavior
    navigation.navigate(route.name);
  };
  
  return <BottomTabBar {...props} navigation={{ ...navigation, navigate: handleTabPress }} />;
}

// 5. Conditionally render tabs based on auth state
function ConditionalTabNavigator() {
  const { isAuthenticated, userRole } = useAuth();
  
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      {isAuthenticated && (
        <>
          <Tab.Screen name="Dashboard" component={DashboardScreen} />
          {userRole === 'admin' && (
            <Tab.Screen name="Admin" component={AdminScreen} />
          )}
        </>
      )}
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

// 6. Reset stack when re-selecting tab
function TabWithStackReset() {
  const navigation = useNavigation();
  
  const handleTabPress = () => {
    // Get the current state of the stack inside this tab
    const stackState = navigation.getState();
    
    // If already on the tab, pop to top
    if (stackState.index > 0) {
      navigation.popToTop();
    }
  };
  
  return (
    <Tab.Screen
      name="Home"
      component={HomeStackScreen}
      listeners={{
        tabPress: (e) => {
          e.preventDefault();
          handleTabPress();
        },
      }}
    />
  );
}

// Best Practices Summary:
// ✅ Use 3-5 tabs maximum for better UX
// ✅ Provide both icons and labels
// ✅ Handle safe area insets (notch, home indicator)
// ✅ Use proper accessibility labels
// ✅ Implement lazy loading for performance
// ✅ Track tab analytics
// ✅ Handle deep linking to specific tabs
// ✅ Reset nested stacks on tab re-selection
// ✅ Support keyboard navigation (iPad)
// ✅ Test on both iOS and Android

Test Your Knowledge

Q1
of 4

Which package is required for bottom tabs in React Navigation 7?

A
@react-navigation/bottom-tabs
B
@react-navigation/tabs
C
@react-navigation/native-tabs
D
@react-navigation/tab-bar
Q2
of 4

What React Navigation 8 feature improves performance by freezing inactive tabs?

A
lazy
B
unmountOnBlur
C
freezeOnBlur
D
detachInactive
Q3
of 4

Which tab bar option shows a notification count on a tab icon?

A
tabBarIcon
B
tabBarNotification
C
tabBarBadge
D
tabBarAlert
Q4
of 4

What is the default maximum number of visible tabs in Material Top Tabs without scrolling?

A
3
B
4
C
5
D
6

Frequently Asked Questions

What's the difference between bottom tabs and top tabs?

Bottom tabs are standard for mobile navigation (iOS and Android) and appear at the bottom of the screen. They're always accessible via thumb reach. Top tabs (Material Design) are common on Android for secondary navigation (e.g., categories within a screen) and can scroll horizontally. Use bottom tabs for primary app navigation, top tabs for content filtering.

How do I hide the tab bar on certain screens?

Use getFocusedRouteNameFromRoute to detect the current route inside nested stacks. Then conditionally set tabBarStyle: { display: 'none' } on the tab screen options. For simple cases, you can also hide the tab bar directly from a screen using navigation.getParent()?.setOptions({ tabBarStyle: { display: 'none' } }).

What's new in React Navigation 8 tabs?

React Navigation 8 introduces native bottom tabs by default (no separate package), iOS 26 liquid glass effects, improved performance with freezeOnBlur, better TypeScript inference, and simplified API. The native implementation uses UITabBarController on iOS and BottomNavigationView on Android for native look and feel.

How do I add badges to tab icons?

Use the tabBarBadge option on Tab.Screen: options={{ tabBarBadge: 5 }}. Update it dynamically using navigation.setOptions({ tabBarBadge: newCount }). For custom badge styling, use tabBarBadgeStyle or implement a custom tab bar component for complete control.

Previous

react native stack nav

Next

react native state

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.