Note: This tutorial assumes you’re already up and running with React Navigation 3.x

I received a design comp this week that included a tab bar with some extra features that React Navigation doesn’t provide out of the box. One of them was a button to trigger a settings drawer. I googled around and found some hacky ways to do it, but I still couldn’t quite match my designs and soon realized I’d be better off building my own.

I thought it’d be a significant amount of work, but it took me about 20 minutes and I ended up with a tab bar that had all the functionality of the default tab bar, and also allowed me to add arbitrary elements and customize to my heart’s content.

Here’s what we’re going to do:

Create a TabBar component to hold our custom tabs

component to hold our custom tabs Create a custom Tab component

component Render a Tab for each route we’ve defined, as well as a custom menu button

for each route we’ve defined, as well as a custom menu button Wire up some animations to highlight the active tab.

This is my entire Navigator — I’m not even going to bother rendering screens, we won’t need ’em:

Notice that I’ve set tabBarComponent: TabBar in the TabNavigatorConfig (the second argument to createMaterialTopTabNavigator ). This is our custom component. We’ll build it now:

Let’s step through the TabBar . React Navigation will pass us the following props:

navigation : We’ll use this prop to call actions on our navigator in order to navigate to other screens or open the drawer.

: We’ll use this prop to call actions on our navigator in order to navigate to other screens or open the drawer. navigationState : Gives us important data about the available routes and current nav state, including a routes array containing all routes we defined in our navigator config and the index of the active route.

: Gives us important data about the available routes and current nav state, including a array containing all routes we defined in our navigator config and the of the active route. position : An Animated.Value that will range from 0 to routes.length as the user navigates between routes. We’ll use this to configure animations within each tab as it focuses and blurs.

The first thing we’ll do is configure the look of the bar itself by styling the outer View (most importantly, giving it a flexDirection: “row" so our tabs render horizontally). Next, we'll loop through navigationState.routes and create a Tab for each route. We’ll build out that Tab component soon.

Check out the onPress that we pass to the Tab . We know the routeName for each Tab since it’s on each route object in the routes array that we’re mapping over, so all we have to do is call navigation.navigate(route.routeName) and React Navigation will handle the actual navigation.

Animations

This part is optional, but route-based animations are such a nice bit of polish I couldn’t pass it up. Remember that the position Animated.Value spans from 0 to routes.length . For each tab we render, we’ll interpolate that value in order to get a value that swings between 0 and 1 depending on whether the user is viewing the screen associated with that tab. Check out the docs on interpolation if this isn’t a familiar concept.

So, if our routes are [feed, profile, inbox] and the user is viewing the feed screen, position will have the value of 0 — the index of feed . When the user navigates to inbox , position will swing through all values between 0 and 2 , and will remain at 2 as long as the user remains on the inbox screen.

However, each individual tab only cares whether the user is viewing its route, not whether the user is viewing index 3 or 6 or any other index. We know each tab’s index in the routes array, so we can pass it a custom Animated.Value that has value 1 when the user is viewing that index, and 0 otherwise:

const focusAnim = position.interpolate({

inputRange: [index - 1, index, index + 1],

outputRange: [0, 1, 0],

})

For example, when the user is viewing inbox , the inbox tab’s animated value is 1 and all other tabs have value 0 . When the user navigates to profile , the profile tab’s value will ramp up to 1 , index will ramp down to 0 , and all other tabs remain unchanged at 0 .

To recap, for each route we map over, we create an animated value that is 0 when that route is not focused and animates to 1 as it becomes focused. We’ll pass that value to each Tab on a prop we’ll call focusAnim and we’ll use it to style each Tab in the next section.

Tab

Finally, we’ll build our Tab :

Let’s wire up that animated value. When each tab is not focused, we want its background color to be transparent, and its text color to be a dark grey. As the Tab becomes focused, we want to apply a tomato-colored background color and change to text color to white for some nicer contrast.

Each Tab’s focusAnim prop swings from 0 to 1 and back to 0 as it focuses and blurs, so now all we have to do is create another interpolation that takes an input range of 0 to 1 and maps that to an output range from “inactive color” to “active color”:

focusAnim.interpolate({

inputRange: [0, 1],

outputRange: ["transparent", "tomato"],

})

The result looks like this — the tab background and the tab text colors animate as I navigate by swiping or by tapping the tab:

Adding a button

We’re almost done! Let’s add that menu button I promised. All we’ll do is render a TouchableOpacity after we map over our routes. We can do anything we want in its onPress —we could call a method on the navigation prop like navigation.openDrawer() , we could dispatch an action to our store, whatever.

Here’s the final TabBar :