DEV Community

Cover image for Animated sliding tab bar in React Native
Oksana Ivanchenko
Oksana Ivanchenko

Posted on • Updated on

Animated sliding tab bar in React Native

Today I'll show you how to do this custom tab bar with sliding animation in React Native.
Sliding animation in React Native

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


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.

Dependencies needed
We will be using react-navigation. We will also install react-navigation-tabs to create the bottom tab bar.

yarn add react-navigation react-navigation-tabs
Enter fullscreen mode Exit fullscreen mode

For this tutorial, I will also be using 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
Enter fullscreen mode Exit fullscreen mode

My folder configuration:

>src
  >components
    >icon.js
    >tabbar.js
    >router.js
  >screens
    >home.js
    >planning.js
    >search.js
    >settings.js
App.js
Enter fullscreen mode Exit fullscreen mode

Creating a custom bottom tab bar

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

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

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:

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

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.

/* App.js */
import React from 'react';
import Router from './src/components/router';

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

export default App;
Enter fullscreen mode Exit fullscreen mode

In src/components/tabbar.js:

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

Here is our custom tab bar. For now, it looks like this:
Custom tab bar without animation settings

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:

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

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

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:

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

Now we need to wrap our Tabbar component in the withDimensions component.

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

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

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

To use it we will need to wrap our App component in a SafeAreaProvider provided by this library.

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

Now in src/components/withDimensions.js:

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

Also, don't forget to change the SafeAreaView importation. Now we will import it from 'react-native-safe-area-view'.

/*src/components/tabbar.js*/
import SafeAreaView from 'react-native-safe-area-view';
Enter fullscreen mode Exit fullscreen mode

That's it! Our tabbar now works on all devices.
Custom tab bar with sliding animation

Top comments (2)

Collapse
 
baptistearnaud profile image
Baptiste Arnaud

Great tuto, merci Oksana :)

Your code is valid with React Navigation v4. I made a similar tutorial for React Navigation v5 here :
dev.to/baptistearnaud/animated-sli...

Collapse
 
bernardbaker profile image
Bernard Baker

Great article. I think navigation is really important in any app and the use of the native animation library is always good practice.