Behind the Scenes: Understanding the Inner Workings of Custom Tab Bars with React Navigation
Introduction
The default tab bar in React Navigation is rather basic, and attempting to customize it can lead to a messy outcome. Fortunately, there is a straightforward way to implement custom tabs using the Material Top Tab Bar in React Native with React Navigation. The setup process is quick and can be completed in just a few minutes. Today in this blog we are going to understand how it works.
I am assuming you did the setup of React Navigation in your project, if not follow the steps to set up React Navigation and create a native stack navigator, it's pretty easy.
To have a tab bar on the top of the screen, that lets us switch between routes by tapping the tabs or swiping horizontally, we have Material Tob Tabs Navigator by React Navigation. Please refer docs.
This is how my App.tsx
looks. It is always a good idea to create separate files for your stack screens. Once your project grows and there are multiple screens the App.tsx
becomes cluttered!
import {NavigationContainer} from '@react-navigation/native';
// ...
function App(): React.JSX.Element {
return (
<NavigationContainer>
<SafeAreaView style={styles.mainContainer}>
<Navigation />
</SafeAreaView>
</NavigationContainer>
);
}
Navigation.tsx
import {createNativeStackNavigator} from '@react-navigation/native-stack';
// ...
const Navigation = () => {
const Stack = createNativeStackNavigator();
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
options={{headerShown: false}}
name="Home"
component={Homescreen}
/>
</Stack.Navigator>
);
};
export default Navigation;
Homescreen.tsx
import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
//...
const Homescreen = () => {
const Tab = createMaterialTopTabNavigator();
return (
<Tab.Navigator
tabBar={props => <CustomTabBar {...props} />}
style={styles.tabStyle}>
<Tab.Screen name="Demo1" component={DemoScreen1} />
<Tab.Screen name="Demo2" component={DemoScreen2} />
<Tab.Screen
options={{title: 'Demo Screen 3'}}
name="Demo3"
component={DemoScreen3}
/>
</Tab.Navigator>
);
};
Here is the custom tabBar function
CustomTabBar.tsx
import {Animated, View, TouchableOpacity} from 'react-native';
export default function CustomTabBar({state, descriptors, navigation, position}: any) {
return (
<View style={{flexDirection: 'row'}}>
{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, route.params);
}
};
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
const inputRange = state.routes.map((_, i) => i);
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map(i => (i === index ? 1 : 0)),
});
return (
<TouchableOpacity
key={route.key}
accessibilityRole="button"
accessibilityState={isFocused ? {selected: true} : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarTestID}
onPress={onPress}
onLongPress={onLongPress}
style={{flex: 1}}>
<Animated.Text style={{color: 'black'}}>{label}</Animated.Text>
</TouchableOpacity>
);
})}
</View>
);
}
Let us break down the function.
Internal working
These 4 parameters are important while building a Custom tab component:
State
Imagine this as a
snapshot
of where you are in your app. It keeps track of which screen you're currently viewing and provides a list of all possible routes.If you just console.log the state, you will get this object, each time you change a screen the state will get updated.
history -> The
history
is like a record or list of all the screens you have visited in the past. Not all parts of the app have this history feature—only certain parts, like tabs or drawers, might keep track of where you've beenIndex -> Position of the focused screen in the routes array.
routesNames -> Name of the screen defined in the navigator.
// console.log(state)
{
"history": [
{
"key": "Demo1-tDHcPmmRmdJs837m1-olK",
"type": "route"
},
{
"key": "Demo3--0m-QGhSnWqixDeKdoS7D",
"type": "route"
}
],
"index": 2,
"key": "tab-rsFLKC9b4gow_h2x6QelX",
"routeNames": [
"Demo1",
"Demo2",
"Demo3"
],
"routes": [
{
"key": "Demo1-tDHcPmmRmdJs837m1-olK",
"name": "Demo1",
"params": undefined
},
{
"key": "Demo2-EimNrVsIA-ZXW8mCSSxua",
"name": "Demo2",
"params": undefined
},
{
"key": "Demo3--0m-QGhSnWqixDeKdoS7D",
"name": "Demo3",
"params": undefined
}
],
"stale": false,
"type": "tab"
}
Descriptors
Think of
descriptors
as a set of instructions for each route. It tells the app how to handle and display each screen, including any special instructions for the tab bar.// console.log(descriptors) "Demo3-4vfsJoiMZndxF-MRA2cpj": { "navigation": { "addListener": [ FunctionaddListener ], "isFocused": [ FunctionisFocused ], "navigate": [ Functionanonymous ], "replace": [ Functionanonymous ], ...and many more }, "options": { "title": "Demo SCreen 3" }, "render": [ Functionrender ], "route": { "key": "Demo3-4vfsJoiMZndxF-MRA2cpj", "name": "Demo3", "params": undefined } }
Navigation
The
navigation
prop is like a remote control for moving around in your app. It gives you buttons to go to different screens, go back, and perform other actions related to navigation.// console.log(navigation) { "addListener": [ FunctionaddListener ], "dispatch": [ Functiondispatch ], "pop": [ Functionanonymous ], "popToTop": [ Functionanonymous ], "push": [ Functionanonymous ], "replace": [ Functionanonymous ], //....and many more }
Position
The position helps in creating cool effects, like making tabs fade in or out based on where you are on the list of screens.
// console.log(position) 0.8722222447395325
Now that we know why each parameter is important, it is easy to understand the code. Let us have a look at onPress
function.
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name, route.params);
}
};
The default behavior is what normally happens when you tap a tab: it takes you to the screen connected to that tab. The code checks if anything wants to stop this default action before allowing it to happen.
Conclusion
In this post, we've explored how a custom tab bar function works in React Navigation, focusing on the Material Top Tab Bar in React Native. By breaking down the essential parameters—state
, descriptors
, navigation
, and position
—we've unveiled the function's operation.
If you have any questions or just want to talk about tech in general, feel free to reach out to me. Hopefully, this helps you understand React Navigation better.
Subscribe to my newsletter
Read articles from Prathamesh Karambelkar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Prathamesh Karambelkar
Prathamesh Karambelkar
Hello there! 👋 I'm a final year student with 6 months of immersive experience as a lead frontend developer. In my tech toolkit, you'll find React, Next.js, React Native, and the artistic touch of Tailwind CSS. 📩 Let's Connect! Whether you're looking to collaborate, network, or just have a tech conversation, reach out.