Animated Sliding tab bar in React Native

Baptiste Arnaud
4 min readApr 8, 2020

I’ll show you in this tutorial how to make this cool animated custom bottom tab bar using React Navigation.

In order to do this, I found this great tutorial by Oksana Ivanchenko :

But unfortunately, it’s already outdated due to the recent publication of React Navigation v5. And it comes with lots of modifications.

Dependencies

npm install @react-navigation/nativenpm install @react-navigation/bottom-tabsexpo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

Custom MenuItem

import React from "react";
import { View } from "react-native";
import { blue, grey } from "../../styles";
import { AntDesign } from "@expo/vector-icons";
type Props = {
iconName: string;
isCurrent?: boolean;
};
export const BottomMenuItem = ({ iconName, isCurrent }: Props) => {
return (
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
}}
>
<AntDesign
name={iconName}
size={32}
style={{ color: isCurrent ? blue : grey }}
/>
</View>
);
};

This represented each item in the tab bar. You can adjust this component with anything you want. Here it requires an iconName in order to display the correct icon and a isCurrent property which changes the icon color if it is currently selected.

The Custom TabBar component

import React, { useState } from "react";
import {
View,
TouchableOpacity,
Dimensions,
StyleSheet,
} from "react-native";
import { BottomTabBarProps } from "@react-navigation/bottom-tabs";
import { BottomMenuItem } from "./BottomMenuItem";
import { blue } from "../../styles";
export const TabBar = ({
state,
descriptors,
navigation,
}: BottomTabBarProps) => {
const totalWidth = Dimensions.get("window").width;
const tabWidth = totalWidth / state.routes.length;
return (
<View style={[style.tabContainer, { width: totalWidth }]}>
<View style={{ flexDirection: "row" }}>
<View style={style.slider}/>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const isFocused = state.index === index;const onPress = () => {
const event = navigation.emit({
type: "tabPress",
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
const onLongPress = () => {
navigation.emit({
type: "tabLongPress",
target: route.key,
});
};
return (
<TouchableOpacity
accessibilityRole="button"
accessibilityStates={isFocused ? ["selected"] : []}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarTestID}
onPress={onPress}
onLongPress={onLongPress}
style={{ flex: 1 }}
key={index}
>
<BottomMenuItem
iconName={label.toString()}
isCurrent={isFocused}
/>
</TouchableOpacity>
);
})}
</View>
</View>
);
};
const style = StyleSheet.create({
tabContainer: {
height: 60,
shadowOffset: {
width: 0,
height: -1,
},
shadowOpacity: 0.1,
shadowRadius: 4.0,
backgroundColor: "white",
borderTopRightRadius: 20,
borderTopLeftRadius: 20,
elevation: 10,
position: "absolute",
bottom: 0,
},
slider: {
height: 5,
position: "absolute",
top: 0,
left: 10,
backgroundColor: blue,
borderRadius: 10,
width: 50
},
});

Connect the custom TabBar to the Navigation System (BottomMenu component)

import React from "react";
import {
createBottomTabNavigator,
BottomTabBarProps,
} from "@react-navigation/bottom-tabs";
import { TabBar } from "./TabBar";
import { AppsScreen } from "../../screens/AppsScreen";
import { DashboardScreen } from "../../screens/DashboardScreen";
import { GroupScreen } from "../../screens/GroupScreen";
import { ProfileScreen } from "../../screens/ProfileScreen";
import { useSafeArea } from "react-native-safe-area-context";
import { View } from "react-native";
export const BottomMenu = () => {
const Tab = createBottomTabNavigator();
return (
<View style={{ flex: 1, position: "relative"}}>
<Tab.Navigator
tabBar={(props: BottomTabBarProps) => <TabBar {...props} />}
>
<Tab.Screen name="search1" component={AppsScreen} />
<Tab.Screen name="dashboard" component={DashboardScreen} />
<Tab.Screen name="profile" component={GroupScreen} />
<Tab.Screen name="user" component={ProfileScreen} />
</Tab.Navigator>
{useSafeArea().bottom > 0 && (
<View
style={{
height: useSafeArea().bottom - 5,
backgroundColor: "white",
}}
/>
)}
</View>
);
};

we use the useSafeArea piece of code in order to render our tab bar higher if there is the horizontal bar on the recent iOS devices for example.

Then you simply have to place this BottomMenu anywhere. In App component for example:

import React from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { NavigationContainer } from "@react-navigation/native";
import {BottomMenu} from "./src/components/BottomMenu/BottomMenu";
export default function App() {
return (
<NavigationContainer>
<SafeAreaProvider>
<BottomMenu/>
</SafeAreaProvider>
</NavigationContainer>
);
}

Now you should have the bottom tab bar on your app but we need to animate the “slider” now.

So in the `TabBar` component, you need:

To add a state variable :

const [translateValue] = useState(new Animated.Value(0));

To change the current View representing the slider to :

<Animated.View
style={[
style.slider,
{
transform: [{ translateX: translateValue }],
width: tabWidth - 20,
},
]}
/>

And finally, in the onPress function, add this piece of code:

Animated.spring(translateValue, {
toValue: index * tabWidth,
velocity: 10,
useNativeDriver: true,
}).start();

--

--