DEV Community

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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" />;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Now we render our Router in AppEntry.js

/* /src/AppEntry.js */

import React from "react";

import { Router } from "./router";

export default () => <Router />;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
/* /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;
Enter fullscreen mode Exit fullscreen mode

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} />
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
/* /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;
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
);
...
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 }
});

...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

And the router config

/* /src/router/router.js */

...

const TabNavigator = createBottomTabNavigator(
  /* screen config ommited */,
  {
    tabBarComponent: TabBar,
    tabBarOptions: {
      activeTintColor: "#eeeeee",
      inactiveTintColor: "#222222"
    }
  }
);

...
Enter fullscreen mode Exit fullscreen mode

Discussion (23)

pic
Editor guide
Collapse
kashifudk profile image
kashifudk

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


Collapse
hrastnik profile image
Mateo Hrastnik Author

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.

Collapse
kashifudk profile image
kashifudk

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

Thread Thread
kashifudk profile image
kashifudk • Edited

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

Custom TabBar Expo Link

Please check and let me know

Thread Thread
kashifudk profile image
kashifudk

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

Thread Thread
hrastnik profile image
Mateo Hrastnik Author

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>
Collapse
alisherakb profile image
Alisher Akbarov • Edited

Help wanted, please!

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

Screenshot

Collapse
hrastnik profile image
Mateo Hrastnik Author

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.

Collapse
alisherakb profile image
Alisher Akbarov • Edited

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

Thread Thread
hrastnik profile image
Mateo Hrastnik Author

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

Collapse
petarprok profile image
Petar Prokopenko

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

Collapse
lalitgvp2 profile image
The third deadly sin⚡️

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?

Collapse
anujcrsdb04 profile image
Anuj Sharma

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

Collapse
hrastnik profile image
Mateo Hrastnik Author

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

Collapse
fr3fou profile image
fr3fou!! 🎀

could you update this for react navigation 5?

Collapse
hrastnik profile image
Mateo Hrastnik Author

Will update soon, when I find the time.

Collapse
fr3fou profile image
fr3fou!! 🎀

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

i fixed a library that presumably followed this tutorial

Collapse
guptavishesh143 profile image
Vishesh Gupta

Hey Guys,
I need help as I am getting error of you are using react-navigation 5 but it seem the component using 4v,
can anyone help me to upgrade the same tabbar

Collapse
wiredmatrix profile image
Benjamin Ikwuagwu

Thanks Mateo for this tutorial. In my case I only want to make homeScreen tab bar background "transparent", and other tab bar screens background to "white". I need your on it please.

Collapse
jgecen profile image
Gécen

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

Collapse
mateusrlima profile image
MateusRLima

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

Collapse
hrastnik profile image
Mateo Hrastnik Author

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

Collapse
thatcharles profile image
Charles Chung

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