Let's Create A Custom Animated Tab Bar With React Native
Mateo Hrastnik
Posted on January 6, 2019
If you've ever felt like the default tab bar component you get from React Navigation looks too bland, or just wanted to create something a bit more modern looking, well, then you're like me. In this guide I'll show you how you can create a custom tab bar to use with React Navigation.
EDIT: I've extended this example and published the code on github. Link to repo
Here's what the end products will look like
Here's how to get there. First let's initialize a new project and install a couple of dependencies. We'll run some commands in the terminal.
$ react-native init CustomTabBar
$ cd CustomTabBar
$ npm install react-navigation react-native-gesture-handler react-native-pose
React Navigation requires react-native-gesture-handler since v3 so we have to install that and react-native-pose is just a great library we're going to use to make animations really simple.
Now there's a linking step needed to make react-native-gesture-handler work on Android. It's all explained on the https://reactnavigation.org/docs/en/getting-started.html#installation, so I'm going to skip the setup part.
Now we can actually start the app and code up the tab bar.
First thing's first - We'll create a directory structure that will help keeping things organized.
/android
/ios
...
/src
/AppEntry.js
/router
/router.js
/index.js
/components
/screens
/index.js
First we'll create a src
directory to separate our code from the other files in the root of the project (package.json, app.json, .gitignore etc.). The screens
, components
and router
directories are self explanatory.
We delete the default App.js
file from the root of the project and change index.js
to import /src/AppEntry.js
/* /index.js */
/** @format */
import { AppRegistry } from "react-native";
import App from "./src/AppEntry";
import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => App);
Now we want to create the router using react-navigation, but first we need to create some dummy screens. We'll create a generic Screen component that takes a name and displays it to emulate multiple screens.
We add some exports to the /src/screens/index.js
file like so
/* /src/screens/index.js */
import React from "react";
import Screen from "./Screen";
export const HomeScreen = () => <Screen name="Home" />;
export const SearchScreen = () => <Screen name="Search" />;
export const FavoritesScreen = () => <Screen name="Favorites" />;
export const ProfileScreen = () => <Screen name="Profile" />;
Now we create the Screen component.
/* /src/screens/Screen.js */
import React from "react";
import { Text, View, StyleSheet } from "react-native";
const S = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#bbbbbb",
justifyContent: "center",
alignItems: "center"
},
text: { fontSize: 28, color: "#222222", textAlign: "center" }
});
const Screen = ({ name }) => (
<View style={S.container}>
<Text style={S.text}>This is the "{name}" screen</Text>
</View>
);
export default Screen;
Time to create the router.
First let's add the export to /src/router/index.js
/* /src/router/index.js */
export { default as Router } from "./router";
Now let's create the basic BottomTabNavigator in router.js
. We'll import our screens and use the createBottomTabNavigator
to create a default tab navigator.
/* /src/router/index.js */
import { createAppContainer, createBottomTabNavigator } from "react-navigation";
import {
HomeScreen,
SearchScreen,
FavoritesScreen,
ProfileScreen
} from "../screens";
const TabNavigator = createBottomTabNavigator({
HomeScreen,
SearchScreen,
FavoritesScreen,
ProfileScreen
});
export default createAppContainer(TabNavigator);
Now we render our Router in AppEntry.js
/* /src/AppEntry.js */
import React from "react";
import { Router } from "./router";
export default () => <Router />;
When we reload our app we should see this screen:
The default tab bar supports icons, so let's add some icons. We'll use ascii characters for this tutorial, but you can use react-native-vector-icons or a custom icon font in a real app.
Let's create an Icon component that accepts props name
and color
and returns the icon.
/* /src/components/index.js */
export { default as Icon } from "./Icon";
/* /src/components/Icon.js */
import React from "react";
import { Text } from "react-native";
const iconMap = {
home: "♡",
search: "♢",
favorites: "♧",
profile: "♤"
};
const Icon = ({ name, color, style, ...props }) => {
const icon = iconMap[name];
return <Text style={[{ fontSize: 26, color }, style]}>{icon}</Text>;
};
export default Icon;
Now we can use this component in our router. We change our screens in router.js
to accept an object with the navigationOptions
prop. The default tab bar passes the tintColor to our icon component so we use that to set our icon color.
/* /src/router/router.js */
const TabNavigator = createBottomTabNavigator({
HomeScreen: {
screen: HomeScreen,
navigationOptions: {
tabBarIcon: ({ tintColor }) => <Icon name="home" color={tintColor} />
}
},
SearchScreen: {
screen: SearchScreen,
navigationOptions: {
tabBarIcon: ({ tintColor }) => <Icon name="search" color={tintColor} />
}
},
FavoritesScreen: {
screen: FavoritesScreen,
navigationOptions: {
tabBarIcon: ({ tintColor }) => <Icon name="favorites" color={tintColor} />
}
},
ProfileScreen: {
screen: ProfileScreen,
navigationOptions: {
tabBarIcon: ({ tintColor }) => <Icon name="profile" color={tintColor} />
}
}
});
Here's what it looks like
Now our tab bar looks a bit better, but it's still the default tab bar from react-navigation. Next we'll add the actual custom tab bar component.
Let's start by creating a custom TabBar component that only renders some text and logs the props so we actually see what props we get from the navigator.
/* /src/components/index.js */
export { default as Icon } from "./Icon";
export { default as TabBar } from "./TabBar";
/* /src/components/TabBar.js */
import React from "react";
import { Text } from "react-native";
const TabBar = props => {
console.log("Props", props);
return <Text>Custom Tab Bar</Text>;
};
export default TabBar;
We have to setup our router so it uses the custom tab bar. We can add the following config as the second parameter to createBottomTabNavigator.
/* /src/router/router.js */
...
import { Icon, TabBar } from "../components";
const TabNavigator = createBottomTabNavigator(
{
HomeScreen: { /* ... */ },
SearchScreen: { /* ... */ }
},
{
tabBarComponent: TabBar,
tabBarOptions: {
activeTintColor: "#4F4F4F",
inactiveTintColor: "#ddd"
}
}
);
...
If we check what our tab bar logged we see we have the navigation state in navigation.state
which also holds the routes. There's also the renderIcon
function, onTabPress
and lots of other stuff we might need. Also we notice how the tabBarOptions
we set in the router config get injected as props to our component.
Now we can start coding our tab bar. To begin, let's try to recreate the default tab bar. We'll set some styling on the container to layout the tab buttons in a row and render a tab button for each route. We can use the renderIcon
function to render the correct icons - digging around through the source showed it expects an object of shape { route, focused, tintColor }
. We add the onPress handlers, and the accessibility labels and voila - we have the default tab bar.
/* /src/components/TabBar.js */
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
const S = StyleSheet.create({
container: { flexDirection: "row", height: 52, elevation: 2 },
tabButton: { flex: 1, justifyContent: "center", alignItems: "center" }
});
const TabBar = props => {
const {
renderIcon,
getLabelText,
activeTintColor,
inactiveTintColor,
onTabPress,
onTabLongPress,
getAccessibilityLabel,
navigation
} = props;
const { routes, index: activeRouteIndex } = navigation.state;
return (
<View style={S.container}>
{routes.map((route, routeIndex) => {
const isRouteActive = routeIndex === activeRouteIndex;
const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;
return (
<TouchableOpacity
key={routeIndex}
style={S.tabButton}
onPress={() => {
onTabPress({ route });
}}
onLongPress={() => {
onTabLongPress({ route });
}}
accessibilityLabel={getAccessibilityLabel({ route })}
>
{renderIcon({ route, focused: isRouteActive, tintColor })}
<Text>{getLabelText({ route })}</Text>
</TouchableOpacity>
);
})}
</View>
);
};
export default TabBar;
Here's how it looks:
Now we know we have the flexibility to create our own tab bar, so we can start actually extending it. We'll use react-native-pose to create an animated view that is going to highlight the active route - let's call this view the spotlight.
First we can remove the label. Then we add an absolute view behind the tab bar that will hold the spotlight. We calculate the offsets for the spotlight using the Dimensions API.
/* /src/components/TabBar.js */
import posed from "react-native-pose";
const windowWidth = Dimensions.get("window").width;
const tabWidth = windowWidth / 4;
const SpotLight = posed.View({
route0: { x: 0 },
route1: { x: tabWidth },
route2: { x: tabWidth * 2 },
route3: { x: tabWidth * 3 }
});
...
const S = StyleSheet.create({
/* ... */
spotLight: {
width: tabWidth,
height: "100%",
backgroundColor: "rgba(128,128,255,0.2)",
borderRadius: 8
}
});
/* ... */
<View style={S.container}>
<View style={StyleSheet.absoluteFillObject}>
<SpotLight style={S.spotLight} pose={`route${activeRouteIndex}`} />
</View>
{routes.map((route, routeIndex) => {
/* ... */
}}
</View>
Here's how it looks:
Note that we never specified the duration and the behavior of the animation. Pose takes care of this for use with reasonable defaults.
Now we'll add some scaling to the active icon. Let's create another posed View.
/* /src/components/TabBar.js */
...
const Scaler = posed.View({
active: { scale: 1.25 },
inactive: { scale: 1 }
});
...
Now we can wrap the icon in our Scaler component like this.
/* /src/components/TabBar.js */
<Scaler style={S.scaler} pose={isRouteActive ? "active" : "inactive"}>
{renderIcon({ route, focused: isRouteActive, tintColor })}
</Scaler>
We get this effect.
Our tab bar is beginning to look pretty good. All that's left to do is polish it up a bit, change the color scheme, tweak our spotlight and our component is completed.
Now, there are things we could improve here. For example, the current implementation assumes there will always be 4 screens in the tab navigator, the spotlight color is hardcoded in the tab bar component, and the styling should be made extensible through the tabBarOptions config on the router, but I'll leave that out for now.
Full source code for the TabBar component
/* /src/components/TabBar.js */
import React from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions
} from "react-native";
import posed from "react-native-pose";
const windowWidth = Dimensions.get("window").width;
const tabWidth = windowWidth / 4;
const SpotLight = posed.View({
route0: { x: 0 },
route1: { x: tabWidth },
route2: { x: tabWidth * 2 },
route3: { x: tabWidth * 3 }
});
const Scaler = posed.View({
active: { scale: 1.25 },
inactive: { scale: 1 }
});
const S = StyleSheet.create({
container: {
flexDirection: "row",
height: 52,
elevation: 2,
alignItems: "center"
},
tabButton: { flex: 1 },
spotLight: {
width: tabWidth,
height: "100%",
justifyContent: "center",
alignItems: "center"
},
spotLightInner: {
width: 48,
height: 48,
backgroundColor: "#ee0000",
borderRadius: 24
},
scaler: { flex: 1, alignItems: "center", justifyContent: "center" }
});
const TabBar = props => {
const {
renderIcon,
activeTintColor,
inactiveTintColor,
onTabPress,
onTabLongPress,
getAccessibilityLabel,
navigation
} = props;
const { routes, index: activeRouteIndex } = navigation.state;
return (
<View style={S.container}>
<View style={StyleSheet.absoluteFillObject}>
<SpotLight style={S.spotLight} pose={`route${activeRouteIndex}`}>
<View style={S.spotLightInner} />
</SpotLight>
</View>
{routes.map((route, routeIndex) => {
const isRouteActive = routeIndex === activeRouteIndex;
const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;
return (
<TouchableOpacity
key={routeIndex}
style={S.tabButton}
onPress={() => {
onTabPress({ route });
}}
onLongPress={() => {
onTabLongPress({ route });
}}
accessibilityLabel={getAccessibilityLabel({ route })}
>
<Scaler
pose={isRouteActive ? "active" : "inactive"}
style={S.scaler}
>
{renderIcon({ route, focused: isRouteActive, tintColor })}
</Scaler>
</TouchableOpacity>
);
})}
</View>
);
};
export default TabBar;
And the router config
/* /src/router/router.js */
...
const TabNavigator = createBottomTabNavigator(
/* screen config ommited */,
{
tabBarComponent: TabBar,
tabBarOptions: {
activeTintColor: "#eeeeee",
inactiveTintColor: "#222222"
}
}
);
...
Posted on January 6, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.