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

hrastnik

Mateo Hrastnik

Posted on January 6, 2019

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

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
💖 💪 🙅 🚩
hrastnik
Mateo Hrastnik

Posted on January 6, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related