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.
- 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.
# 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
- 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.
// 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
- 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.
// 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> ); }
- Custom Tab Bar Component
Create fully customized tab bars with animations, custom buttons, and unique styling for brand-specific designs.
// 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', }, });
- 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.
// 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' }, });
- Nested Tab Navigators
Combine tab navigators with stack navigators for complex navigation patterns. Tabs can contain stacks, and stacks can contain tabs.
// 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), // })} // />
- TypeScript Integration
Full TypeScript support for tab navigators ensures type safety for route names and parameters.
// 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; }
- Tab Navigator Performance
Optimize tab navigator performance with lazy loading, screen freezing, and proper memoization.
// 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
- Advanced Tab Bar Animations
Create animated tab bars with spring animations, floating effects, and custom transitions.
// 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, }, });
- Best Practices & Common Patterns
Production-proven patterns for tab navigator implementation.
// 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