DEV Community

loading...

Animated Sliding tab bar in React Native

Baptiste Arnaud
・3 min read

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

typescript-starter dark logo

In order to do this, I found this great tutorial by @ksushiva.

https://dev.to/ksushiva/animated-sliding-tab-bar-in-react-native-56nb

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/native

npm install @react-navigation/bottom-tabs

expo 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();

You can find the entire code example in this repository:
https://github.com/baptisteArnaud/animated-bottom-tab-react-native-example

Feel free to post a comment on this tutorial if you need help.

Discussion (1)

Collapse
awalariansyah profile image
awalariansyah

Thank you so much, bro.

I just want to ask one thing, how can I modify for example middle menu item to be a big button and overflowing the tab bar?
I've able to do that in default bottom tabs, but in this case still figuring it out.