Tab Bar

The TabBar component gives you a native tabbed interface in iOS and Android. You create one TabBarItem child per tab and idenfity them using the title prop. On the 'contacts' scene in our email example, we separate the personal and work contacts by having a separate tab for each. We set the primary prop to false because these aren't top level navigation tabs.

import {TabBar, TabBarItem} from 'navigation-react-native';

const Contacts = () => (
  <TabBar primary={false}>
    <TabBarItem title="Personal">
      <ScrollView />
    </TabBarItem>
    <TabBarItem title="Work">
      <ScrollView />
    </TabBarItem>
  </TabBar>
);

Adding Top Level Navigation Tabs

Top level navigation tabs are different. On iOS, and sometimes Android, they stay at the bottom of the screen the whole time. Each tab has its own stack of scenes. You navigate forward and back within the tab. To recreate these you render a TabBar component and set its primary prop to true. Then you assign each TabBarItem a NavigationStack component with its own StateNavigator.

Let's add top level tabs to our email example. One tab for the user's inbox and another for their list of contacts. We create a new StateNavigator for each of the tabs. The StateNavigator for the 'inbox' tab has the 'inbox', 'mail' and 'compose' States that we're already familiar with. The StateNavigator for the 'contacts' tab contains two States so the user can view all their contacts and edit the details of an individual contact.

const contactsNavigator = new StateNavigator([
  {key: 'contacts'},
  {key: 'contact', trackCrumbTrail: true}
]);

We create a scene that returns a TabBar component. We give each tab its own NavigationStack and StateNavigator. For example, we give the 'contacts' tab the contactsNavigator we just created. To make them look like top level tabs, we assign each one an image as well as a title. We hide the navigation bar otherwise, on iOS, it would block the navigation bar of the scenes inside the tabs.

const Tabs = () => (
  <>
    <NavigationBar hidden={true} />
    <TabBar primary={true}>
      <TabBarItem title="Inbox" image={require('./home.png')}>
        <NavigationHandler stateNavigator={inboxNavigator}>
          <NavigationStack>
            <Scene stateKey="inbox"><Inbox /></Scene>
            <Scene stateKey="mail"><Mail /></Scene>
            <Scene stateKey="compose"><Compose /></Scene>
          </NavigationStack>
        </NavigationHandler>
      </TabBarItem>
      <TabBarItem title="Contacts" image={require('./contacts.png')}>
        <NavigationHandler stateNavigator={contactsNavigator}>
          <NavigationStack>
            <Scene stateKey="contacts"><Contacts /></Scene>
            <Scene stateKey="contact"><Contact /></Scene>
          </NavigationStack>
        </NavigationHandler>
      </TabBarItem>
    </TabBar>
  </>
);

We replace the States in the top-level StateNavigator with a single one for the 'tabs' scene. Remember that the 'inbox', 'mail' and 'compose' States now live inside the StateNavigator of the 'inbox' tab. There's only one scene in the top-level NavigationStack and it renders the Tabs component.

const stateNavigator = new StateNavigator([
  {key: 'tabs'}
]);

<NavigationStack>
  <Scene stateKey="tabs"><Tabs /></Scene>
</NavigationStack>

Note

We could assign the number of unread emails to the badge prop of the 'inbox' TabBarItem. But the badge won't display on Android unless we change to a MaterialComponents theme in the styles.xml, for example, Theme.MaterialComponents.Light.NoActionBar or Theme.Material3.Light.NoActionBar

Scrolling the Navigation Bar on Android

By wrapping the contacts scene in a CoordinatorLayout component we prevent the navigation bar from getting in the way on Android. The navigation bar gradually scrolls offscreen as the user moves down through their contacts. As soon as the user starts to scroll back up the navigation bar reappears. We render an empty TabBar inside the navigation bar to prevent the tabs scrolling off the screen, too. This functionality is only available when nested scrolling is enabled on the React Native ScrollViews.

<CoordinatorLayout>
  <NavigationBar title="Contacts">
    <TabBar primary={false} />
  </NavigationBar>
  <TabBar primary={false}>
    <TabBarItem title="Personal">
      <ScrollView nestedScrollEnabled={true} />
    </TabBarItem>
    <TabBarItem title="Work">
      <ScrollView nestedScrollEnabled={true} />
    </TabBarItem>
  </TabBar>
</CoordinatorLayout>

Any content inside a CollapsingBar component will collapse as the user scrolls down. We can spruce up our contacts scene by placing an image in the navigation bar. We use an Animated.Image component so we can add a parallax scrolling effect. As the navigation bar collapses the offsetChanged event fires and we scale and reposition the image to match.

<CoordinatorLayout>
  <NavigationBar
    style={{height: 240}}
    onOffsetChanged={Animated.event(
        [{nativeEvent: {offset}}]
      )}>
    <CollapsingBar>
      <Animated.Image style={{
        transform: [{scaleY}, {translateY}]}} 
      }} />
    </CollapsingBar>
    <TabBar primary={false} />
  </NavigationBar>
</CoordinatorLayout>