loading...

Let's Create A Custom Animated Tab Bar With React Native

hrastnik profile image Mateo Hrastnik Updated on ・7 min read

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

Custom tab bar with animation

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:

Default tab bar navigation

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

Default tab bar with icons

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:

Custom tab bar - Default look

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:

Tab bar with animation

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.

Animated tab bar with scaling

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.

Final product

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 by:

hrastnik profile

Mateo Hrastnik

@hrastnik

Tells computers what to do, and sometimes they even do it.

Discussion

markdown guide
 

Help Needed, Using your example i have build a custom tab bar but for some reason the text on the android is hiding behind the spotlight view. Works if on the ios but on android its not displaying the text of the selected tab.

Here is my tabbar.js





{routes.map((route, routeIndex) => {
const isRouteActive = routeIndex === activeRouteIndex;
const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;
return (
key={routeIndex}
style={S.tabButton}
onPress={() => {
onTabPress({ route });
}}
onLongPress={() => {
onTabLongPress({ route });
}}
accessibilityLabel={getAccessibilityLabel({ route })}
>


{/* {renderIcon({ route, focused: isRouteActive, tintColor })} */}
{getLabelText({ route })}



);
})}


EN


AR


 

Try setting a high zIndex on your text, or a negative zIndex on the spotlight component.

Cant really help you out there. If you provide a reproducible issue in a snack (snack.expo.io) I can take a look at the code.

 

Tried this... I didn't help will provide you an expo link.

Here is an Expo Link... The problem still exists in the expo.

Custom TabBar Expo Link

Please check and let me know

@hrastnik Please check the expo and let me know if there is something that i'm missing. Really appreciate any help...

Hey @kashifudk sorry for the late reply. For some reason I didn't get any notifications when you replied.
I went over the code, and as I said it's an issue with zIndex.

You should change the style off the View wrapping the Spotlight and add zIndex: 0

Change this:

<View style={StyleSheet.absoluteFillObject}>
    <SpotLight style={S.spotLight} pose={`route${activeRouteIndex}`}/>
</View>

to this:

<View style={{...StyleSheet.absoluteFillObject, zIndex: 0}}>
    <SpotLight style={S.spotLight} pose={`route${activeRouteIndex}`}/>
</View>
 

Help wanted, please!

How can I make a draggable tab? So that on dragging to certain x,y it opens another screen?

Screenshot

 

Maybe you can use draggable and then detect if div passes certain y position to redirect to state?

 

Not sure I understand what you mean. I think you can use the MaterialTabNavigator if you want to be able to use the swipe gesture to change tabs.

 

I have three tabs, one of them is draggable from its initial position.

InitialPosition of centerTab = x: 0, y: 250:

Screenshot 1

SnapPoint of CenterTab = x: 0, y: 72 , i.e can be dragged to this Y value and open new screen:

Screenshot 2

No idea how to help you. Perhaps Reanimated / Animatable or something like that.

 

The component for route 'tabBarOptions' must be a React component. For example:

import MyScreen from './MyScreen';
...
tabBarOptions: MyScreen,
}

You can also use a navigator:

import MyNavigator from './MyNavigator';
...
tabBarOptions: MyNavigator,
}

This is the error that I got trying to run this code. Any ideas?

 

You probably missed something. Check the code I provided for more info.

 

You are not implementing the class that you are using or the name and location of the class must be wrong.

 

Great work im sriram fullstack javascript developer I have a whatsapp group dedicated to coders to collaborate on fun hobby projects if you wanna join please follow the group link chat.whatsapp.com/Hoa6dNwsG4RJOgN9...

 

could you update this for react navigation 5?

 

Will update soon, when I find the time.

 

github.com/torgeadelin/react-nativ...

i fixed a library that presumably followed this tutorial

 

Thank you for this awesome and complete tutorial! Keep it up!!!

 

Hello Mateo, could you pls tell how to change the label too ?

 

You can set tabBarLabel on the tabs navigationOptions. Not sure if you'll need to add something to actually render the label.

 

Best article I read until the memento about navigation, in addition it teaches about project organization. Congratulations!!!