Skip to main content

Tab Navigation

Tab navigation displays a tab bar at the bottom of the screen, allowing users to quickly switch between top-level screens.

Basic Tabs

Create a tab layout:
app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return <Tabs />;
}
Add tab screens:
app/
  (tabs)/
    _layout.tsx
    index.tsx       # Home tab
    explore.tsx     # Explore tab
    profile.tsx     # Profile tab
From the source (Tabs.tsx:1-9):
import Tabs from './TabsClient';
import { Screen } from '../views/Screen';

Tabs.Screen = Screen;

export { Tabs };
export default Tabs;

Configure Tabs

Customize tab appearance:
app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#f4511e',
        tabBarStyle: {
          backgroundColor: '#fff',
        },
        headerShown: false,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: 'Explore',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Tab Icons

Using Icon Libraries

import { Ionicons, MaterialIcons, FontAwesome } from '@expo/vector-icons';

<Tabs.Screen
  name="home"
  options={{
    tabBarIcon: ({ color, size }) => (
      <Ionicons name="home" size={size} color={color} />
    ),
  }}
/>

<Tabs.Screen
  name="search"
  options={{
    tabBarIcon: ({ color, size }) => (
      <MaterialIcons name="search" size={size} color={color} />
    ),
  }}
/>

Custom Icons

import { Image } from 'react-native';

<Tabs.Screen
  name="home"
  options={{
    tabBarIcon: ({ color, focused }) => (
      <Image
        source={focused ? require('./icon-active.png') : require('./icon.png')}
        style={{ width: 24, height: 24, tintColor: color }}
      />
    ),
  }}
/>

Tab Badge

Show notification badges:
app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { useState, useEffect } from 'react';

export default function TabLayout() {
  const [unreadCount, setUnreadCount] = useState(0);
  
  useEffect(() => {
    // Fetch unread notifications
    fetchUnreadCount().then(setUnreadCount);
  }, []);
  
  return (
    <Tabs>
      <Tabs.Screen
        name="notifications"
        options={{
          title: 'Notifications',
          tabBarBadge: unreadCount > 0 ? unreadCount : undefined,
          tabBarIcon: ({ color }) => (
            <Ionicons name="notifications" size={24} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Hide Tab Bar

Hide on Specific Screens

app/details.tsx
import { Tabs } from 'expo-router';

export default function Details() {
  return (
    <>
      <Tabs.Screen options={{ href: null }} />
      <View>
        <Text>Details Screen</Text>
      </View>
    </>
  );
}

Hide Programmatically

import { Tabs } from 'expo-router';
import { useEffect, useState } from 'react';

export default function Screen() {
  const [showTabs, setShowTabs] = useState(true);
  
  return (
    <>
      <Tabs.Screen
        options={{
          tabBarStyle: showTabs ? undefined : { display: 'none' },
        }}
      />
      <ScrollView
        onScroll={(e) => {
          // Hide tabs when scrolling down
          const scrollY = e.nativeEvent.contentOffset.y;
          setShowTabs(scrollY < 50);
        }}
      >
        {/* Content */}
      </ScrollView>
    </>
  );
}

Tab Press Handling

Custom Tab Press

import { Tabs } from 'expo-router';
import { router } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="search"
        options={{
          tabBarIcon: ({ color }) => <Ionicons name="search" />,
        }}
        listeners={{
          tabPress: (e) => {
            e.preventDefault();
            // Custom action
            router.push('/search-modal');
          },
        }}
      />
    </Tabs>
  );
}

Nested Navigation

Add stacks within tabs:
app/
  (tabs)/
    _layout.tsx
    home/
      _layout.tsx     # Stack in home tab
      index.tsx
      details.tsx
    profile/
      _layout.tsx     # Stack in profile tab
      index.tsx
      settings.tsx
app/(tabs)/home/_layout.tsx
import { Stack } from 'expo-router';

export default function HomeStack() {
  return (
    <Stack>
      <Stack.Screen
        name="index"
        options={{ title: 'Home' }}
      />
      <Stack.Screen
        name="details"
        options={{ title: 'Details' }}
      />
    </Stack>
  );
}

Dynamic Tabs

Conditional Tabs

app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { useAuth } from '../../context/auth';

export default function TabLayout() {
  const { user } = useAuth();
  
  return (
    <Tabs>
      <Tabs.Screen name="home" />
      <Tabs.Screen name="explore" />
      
      {user?.isPremium && (
        <Tabs.Screen
          name="premium"
          options={{ title: 'Premium' }}
        />
      )}
      
      <Tabs.Screen name="profile" />
    </Tabs>
  );
}

Tab Bar Styling

Position and Appearance

<Tabs
  screenOptions={{
    tabBarStyle: {
      backgroundColor: '#fff',
      borderTopColor: '#e0e0e0',
      borderTopWidth: 1,
      height: 60,
      paddingBottom: 8,
      paddingTop: 8,
    },
    tabBarActiveTintColor: '#f4511e',
    tabBarInactiveTintColor: '#999',
    tabBarLabelStyle: {
      fontSize: 12,
      fontWeight: '600',
    },
  }}
/>

Hide Labels

<Tabs
  screenOptions={{
    tabBarShowLabel: false,
  }}
/>

Custom Tab Bar

import { Tabs } from 'expo-router';
import { View, Pressable, Text } from 'react-native';

function CustomTabBar({ state, descriptors, navigation }) {
  return (
    <View style={{ flexDirection: 'row' }}>
      {state.routes.map((route, index) => {
        const { options } = descriptors[route.key];
        const isFocused = state.index === index;
        
        return (
          <Pressable
            key={route.key}
            onPress={() => navigation.navigate(route.name)}
            style={{ flex: 1, alignItems: 'center', padding: 16 }}
          >
            {options.tabBarIcon?.({ focused: isFocused })}
            <Text>{options.title}</Text>
          </Pressable>
        );
      })}
    </View>
  );
}

export default function TabLayout() {
  return (
    <Tabs tabBar={(props) => <CustomTabBar {...props} />}>
      <Tabs.Screen name="home" />
      <Tabs.Screen name="profile" />
    </Tabs>
  );
}

Common Patterns

Tab with Modal

app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="home" />
      <Tabs.Screen
        name="create"
        options={{
          tabBarIcon: ({ color }) => <Ionicons name="add" />,
        }}
        listeners={{
          tabPress: (e) => {
            e.preventDefault();
            router.push('/create-modal');
          },
        }}
      />
      <Tabs.Screen name="profile" />
    </Tabs>
  );
}

Scrolling Header in Tab

app/(tabs)/home/index.tsx
import { useState } from 'react';
import { ScrollView } from 'react-native';
import { Tabs } from 'expo-router';

export default function Home() {
  const [headerShown, setHeaderShown] = useState(true);
  
  return (
    <>
      <Tabs.Screen
        options={{
          headerShown,
        }}
      />
      <ScrollView
        onScroll={(e) => {
          const y = e.nativeEvent.contentOffset.y;
          setHeaderShown(y < 50);
        }}
      >
        {/* Content */}
      </ScrollView>
    </>
  );
}

Best Practices

Limit Tab Count

// Good: 3-5 tabs
<Tabs>
  <Tabs.Screen name="home" />
  <Tabs.Screen name="search" />
  <Tabs.Screen name="notifications" />
  <Tabs.Screen name="profile" />
</Tabs>

// Avoid: Too many tabs
<Tabs>
  {/* 7+ tabs - consider using drawer */}
</Tabs>

Use Clear Icons

// Good: Standard, recognizable icons
tabBarIcon: () => <Ionicons name="home" />
tabBarIcon: () => <Ionicons name="search" />

// Avoid: Unclear or custom icons
tabBarIcon: () => <CustomIcon name="unclear" />

Consistent Tab Order

// Good: Consistent order
<Tabs>
  <Tabs.Screen name="home" />      {/* Always first */}
  <Tabs.Screen name="search" />
  <Tabs.Screen name="profile" />  {/* Always last */}
</Tabs>

Next Steps

Stack Navigation

Add stack navigation in tabs

Native Tabs

Use native iOS/Android tabs

Drawer Navigation

Alternative to tabs

Layouts

Understand tab layouts