Animated sliding tab bar in React Native
Tags: reactnative, tutorial, tabbar, navigation
Published: 2020-01-26 14:34:14

---
title: Animated sliding tab bar in React Native
published: true
description: Animated sliding tab bar in React Native using react-navigation and Animated API
tags: #reactnative #tutorial #tabbar #navigation 
cover_image: https://thepracticaldev.s3.amazonaws.com/i/39s2l3p6jvioxzjp8uxr.png
---
Today I'll show you how to do this custom tab bar with sliding animation in React Native.
![Sliding animation in React Native](https://thepracticaldev.s3.amazonaws.com/i/dv7v5v3q3egc97vqes52.gif)


I came across this amazing tutorial written by Mateo Hrastnik that helps us achieve exactly what we need. 

{% link https://dev.to/hrastnik/lets-create-a-custom-animated-tab-bar-with-react-native-3496 %}
**Problem:** In this tutorial, the author uses a library called *react-native-pose* to animate the tab bar. On 15th January 2020 the creators of this library announced that it will no longer be maintained and that it is now deprecated. We need to find another way to animate the tab bar. It turned out that it is simple to do with the native Animated API. That's what I'll show you in this tutorial. Also, I'll show you how to manage the orientation change. If you are too lazy to read it all, you can jump directly into the [GitHub repository](https://github.com/ksushiva/animated-tabbar-react-native). 

**Dependencies needed**
We will be using [react-navigation](https://reactnavigation.org/). We will also install react-navigation-tabs to create the bottom tab bar.
```
yarn add react-navigation react-navigation-tabs
``` 
For this tutorial, I will also be using [FontAwesome](https://github.com/FortAwesome/react-native-fontawesome) for the icons. If you want to do the same, you need to install all the needed dependencies.
```
yarn add react-native-svg @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-native-fontawesome
```
**My folder configuration:**
 
```
>src
  >components
    >icon.js
    >tabbar.js
    >router.js
  >screens
    >home.js
    >planning.js
    >search.js
    >settings.js
App.js
```


#Creating a custom bottom tab bar

I will create a router in *src/components/router.js*.

```javascript
/* src/components/router.js */
import React from 'react';
import {createAppContainer} from 'react-navigation';
import {createBottomTabNavigator} from 'react-navigation-tabs';
import Home from '../screens/home';
import Settings from '../screens/settings';
import Search from '../screens/search';
import Planning from '../screens/planning';
import Icon from './icon';
import TabBar from './tabbar';

const Router = createBottomTabNavigator(
  {
    Home: {
      screen: Home,
      navigationOptions: {
        tabBarIcon: ({tintColor}) => <Icon name="home" color={tintColor} />,
      },
    },
    Planning: {
      screen: Planning,
      navigationOptions: {
        tabBarIcon: ({tintColor}) => <Icon name="planning" color={tintColor} />,
      },
    },
    Search: {
      screen: Search,
      navigationOptions: {
        tabBarIcon: ({tintColor}) => <Icon name="search" color={tintColor} />,
      },
    },
    Settings: {
      screen: Settings,
      navigationOptions: {
        tabBarIcon: ({tintColor}) => <Icon name="settings" color={tintColor} />,
      },
    },
  },
  {
    tabBarComponent: TabBar,
    tabBarOptions: {
      activeTintColor: '#2FC7FF',
      inactiveTintColor: '#C5C5C5',
    },
  },
);

export default createAppContainer(Router);
```
Basically in this code, we are defining our routes and telling that we will be using a custom TabBar component for our tab bar.

In *src/components/icon.js*:
```javascript
/* src/components/icon.js */
import React from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome';
import {
  faHome,
  faCog,
  faSearch,
  faClock,
} from '@fortawesome/free-solid-svg-icons';

const icons = {
  home: faHome,
  search: faSearch,
  planning: faClock,
  settings: faCog,
};

const Icon = ({name, color}) => {
  return (
    <FontAwesomeIcon icon={icons[name]} style={{color: color}} size={20} />
  );
};

export default Icon;
```
Depending on the name of the route that we passed in props, the icon will be different for each tab.

Now we need to import our router in App.js.
```javascript
/* App.js */
import React from 'react';
import Router from './src/components/router';

const App = () => {
  return (
     <Router />
  );
};

export default App;
```

In *src/components/tabbar.js*:
```javascript
/* src/components/tabbar.js */
import React from 'react';
import {View, TouchableOpacity, StyleSheet, SafeAreaView, Dimensions} from 'react-native';

const S = StyleSheet.create({
  container: {
    flexDirection: 'row',
    height: 54,
    borderTopWidth: 1,
    borderTopColor: '#E8E8E8',
  },
  tabButton: {flex: 1, justifyContent: 'center', alignItems: 'center'},
  activeTab: {
    height: '100%',
    alignItems: 'center',
    justifyContent: 'center',
  },
  activeTabInner: {
    width: 48,
    height: 48,
    backgroundColor: '#E1F5FE',
    borderRadius: 24,
  },
});

const TabBar = props => {
  const {
    renderIcon,
    activeTintColor,
    inactiveTintColor,
    onTabPress,
    onTabLongPress,
    getAccessibilityLabel,
    navigation,
  } = props;

  const {routes, index: activeRouteIndex} = navigation.state;
  const totalWidth = Dimensions.get("window").width;
  const tabWidth = totalWidth / routes.length;

  return (
    <SafeAreaView>
      <View style={S.container}>
        <View>
          <View style={StyleSheet.absoluteFillObject}>
            <View
              style={[S.activeTab, { width: tabWidth }]}>
              <View style={S.activeTabInner} />
            </View>
          </View>
        </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})}>
              {renderIcon({route, focused: isRouteActive, tintColor})}
            </TouchableOpacity>
          );
        })}
      </View>
    </SafeAreaView>
  );
};

export default TabBar;
```
Here is our custom tab bar. For now, it looks like this:
![Custom tab bar without animation settings](https://thepracticaldev.s3.amazonaws.com/i/aw8z2zbpi4elwb7p18km.gif)

Now we need to animate our activeTab.
#Animating active tab
To animate the circle that indicates the selected tab, we will be using the **Animated** library which is included in react-native. Our circle should move horizontally from one active tab to another, so we will be using the **translateX** transform value.

In *src/components/tabbar.js*:
```javascript
...
/* src/components/tabbar.js */
import {useState} from 'react';
import {Animated} from 'react-native';

const TabBar = props => {
  ...
  const [translateValue] = useState(new Animated.Value(0)); 
// When the user opens the application, it's the first tab that is open.
// The initial value of translateX is 0.

  const onTabBarPress = (route, routeIndex) => {
    onTabPress(route); // function that will change the route;
    Animated.spring(translateValue, {
      toValue: routeIndex * tabWidth,
// The translateX value should change depending on the chosen route
      velocity: 10,
      useNativeDriver: true,
    }).start(); // the animation that animates the active tab circle
  };

  return (
    <SafeAreaView>
      <View style={S.container}>
        <View>
          <View style={StyleSheet.absoluteFillObject}>
            <Animated.View
              style={[
                S.activeTab,
                {
                  width: tabWidth,
                  transform: [{translateX: translateValue}],
                },
              ]}>
              <View style={S.activeTabInner} />
            </Animated.View>
 {/* the container that we animate */}
          </View>
        </View>
        {routes.map((route, routeIndex) => {
          const isRouteActive = routeIndex === activeRouteIndex;
          const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;

          return (
            <TouchableOpacity
              key={routeIndex}
              style={S.tabButton}
              onPress={() => {
                onTabBarPress({route}, routeIndex);
              }}
              onLongPress={() => {
                onTabLongPress({route});
              }}
              accessibilityLabel={getAccessibilityLabel({route})}>
              {renderIcon({route, focused: isRouteActive, tintColor})}
            </TouchableOpacity>
{/* the onPress function changed. We will now use the onTabPress function that we created.
We will send the route that was selected and its index */}

          );
        })}
      </View>
    </SafeAreaView>
  );
};

export default TabBar;
```
That's it. Now we have a desirable animation. But if we change the orientation of our phone, it will be broken as the width of our tab won't be changed dynamically.
![Custom tab bar with an orientation problem](https://thepracticaldev.s3.amazonaws.com/i/5noj5wc5rlqu8aeghpwy.gif)
# Managing the orientation change
To do this we will create a High Order Component. As it's written in the documentation, HOC is a function that takes a component and returns a new component. 
In *src/components* we are creating *with-dimensions.js*:
```javascript
/* src/components/with-dimensions.js */
import React, {useEffect, useState} from 'react';
import {Dimensions} from 'react-native';

const withDimensions = BaseComponent => props => {
  const [dimensions, setDimensions] = useState({
    width: Dimensions.get('window').width,
  });
//setting the initial width;


  const handleOrientationChange = ({window}) => {

    const {width} = window;
    setDimensions({width});
  };

  useEffect(() => {
    Dimensions.addEventListener('change', handleOrientationChange);
//when the component is mounted and the dimensions change, we will go to the handleOrientationChange function; 
    return () =>
      Dimensions.removeEventListener('change', handleOrientationChange);
    // when the component is unmounted, we will remove the event listener; 
  }, []);

  return (
        <BaseComponent
          dimensions={{width: dimensions.width}}
          {...props}
        />
  );
};

export default withDimensions;
```
Now we need to wrap our Tabbar component in the withDimensions component.
```javascript
/*src/components/tabbar.js*/
...
import {useEffect} from 'react';
import withDimensions from './with-dimensions';

...

const TabBar = props => {
  const {
    renderIcon,
    activeTintColor,
    inactiveTintColor,
    onTabPress,
    onTabLongPress,
    getAccessibilityLabel,
    navigation,
    dimensions,
  } = props;
// adding dimensions in props

//const totalWidth = Dimensions.get("window").width;
  const tabWidth = dimensions.width / routes.length;

  useEffect(() => {
    translateValue.setValue(activeRouteIndex * tabWidth);
  }, [tabWidth]);
// whenever the tabWidth changes, we also change the translateX value

  ...

  return (
    ...
  );
};

export default withDimensions(TabBar);
```
Now it works perfectly on all the Android phones and iPhones without a notch. But if you have an iPhone 10+, you will have a bug in the horizontal mode.
![iPhone 10+ problem](https://thepracticaldev.s3.amazonaws.com/i/qqqun5dnl1mdjwi6pcdm.gif)
# Fixing SafeAreaView bug for the iPhone 10+
The problem is that **{ Dimensions } from 'react-native'*** sends us the total width of the device and not the width of the SafeAreaView. To fix it we will be using [react-native-safe-area-view](https://github.com/react-native-community/react-native-safe-area-view). It will help us know the *padding* of the SafeAreaView so we can include it in the width calculation.
```
yarn add react-native-safe-area-view react-native-safe-area-context
```
To use it we will need to wrap our App component in a SafeAreaProvider provided by this library.
```javascript
/* App.js */

import React from 'react';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import Router from './src/components/router';

const App = () => {
  return (
    <SafeAreaProvider>
      <Router />
    </SafeAreaProvider>
  );
};

export default App;
```
Now in *src/components/withDimensions.js*:
```javascript
/*src/components/withDimensions.js*/
...
import {SafeAreaConsumer} from 'react-native-safe-area-context';

...
return (
    <SafeAreaConsumer>
      {insets => (
        <BaseComponent
          dimensions={{width: dimensions.width - insets.left - insets.right}}
          {...props}
        />
      )}
    </SafeAreaConsumer>
  );
{/*  when we are calculating the width, we substract the padding depending on the iPhone model */}
```
Also, don't forget to change the SafeAreaView importation. Now we will import it from **'react-native-safe-area-view'**.
```javascript
/*src/components/tabbar.js*/
import SafeAreaView from 'react-native-safe-area-view';
```
That's it! Our tabbar now works on all devices.
![Custom tab bar with sliding animation](https://thepracticaldev.s3.amazonaws.com/i/r09kfkfqvooee1jpipdc.gif)