Animating the bottom tab navigator in React Native in 2021

In this tutorial you will learn how flexible the bottom tab navigation can be in React Native. You can customise more than it seems at first. We will focus on the custom animations to interpolate colour, change size and position of the selected tab.

This demonstration uses a custom icon font. I have explained the way you can create and load your own in this article.

For the code, clone the repo: https://github.com/rolandtreiber/rn-bottom-tabs-tutorial

What we are building

We will be using the built in animation library as well as the bottom tab navigation to build this custom beautiful smooth animated navigation.

Note that when you select an item, it changes colour, size and position.

Prerequisites

To achieve the same result, you should have NPM and the expo-cli installed. In order to run your app on iOS device or simulator and compile your code for Apple devices requires either Mac with Xcode or expo cloud. For Android, you’ll need Android Studio. Find more information on this in the docs.

Once you have all this, you are all set to install and work on your project.

Github

As a starting point, you may use the repo from my custom icon font tutorial here or you can download the final project here.

Packages

To make the bottom tab navigation work, you want to run the following commands to install the packages:

npm i @react-navigation/native
npm i @react-navigation/stack
npm i @react-navigation/bottom-tabs

For this demonstration, I will also use a custom font that I have created earlier. To load this font, let’s install expo-font and expo-app-loading

npm i expo-font
npm i expo-app-loading

I also like styled components quite a bit, so let’s install it as well. If you are not aware of the styled components library and how awesome it is, then it is time to check it out!

npm i styled-components

The screens

To navigate between screens, we’ll need to have a few. For this exercise, we’ll go with a generic screen component that simply displays a title and a content supplied to it as props.

The navigation

The app navigation in our case has two parts: a stack navigation where the bottom tab navigator is one of the screens. It is not strictly necessary to do it this way, however it comes with the benefit of being able to add pages that are not included in the bottom tab navigation. This is usually the case with larger scale applications, so to help you grasp the concept, it is laid out this way.

The Stack Navigator

This is a simplest form of a Stack Navigator in React Native. Nothing fancy, however if you copy the code and try to run the the app, it will fail missing the BottomTabNavigator component. So let’s create it as the next step.

The Bottom Tab Navigator

So far what we have been working on was the preparation part simply setting the scene for implementing our own customised bottom tab navigator.

This is where the actual magic happens, so first I’ll copy the finished file here and then explain how it works in detail.

If you scroll down, you’ll find the detailed explanation of the following sections:

  1. Loading your icons
  2. Creating the navigator and loading the pages
  3. Creating the animations
    - Mass creation
    - Size and Position
    - Color interpolation
  4. Animated styles
import React, {useEffect, useState} from "react";
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
import {Animated, TouchableHighlight} from "react-native";
import Screen from "../screens/GenericScreen";
import styled from "styled-components/native";
import AppLoading from 'expo-app-loading';
import {useFonts} from 'expo-font';
import MyCustomIconSet from '../assets/fonts/MyIconSet.ttf'

const Tab = createBottomTabNavigator()

const BottomTabNavigatorArticle = () => {
const [focusedTab, setFocusedTab] = useState(1)
const mappable = [0, 1, 2, 3, 4]

const colors = mappable.map((item, index) => {
return useState(index === focusedTab ? new Animated.Value(1) : new Animated.Value(0))[0]
})

const iconSizes = mappable.map((item, index) => {
return useState(index === focusedTab ? new Animated.Value(40) : new Animated.Value(28))[0]
})

const boxSizes = mappable.map((item, index) => {
return useState(index === focusedTab ? new Animated.Value(70) : new Animated.Value(50))[0]
})

const topMargins = mappable.map((item, index) => {
return useState(index === focusedTab ? new Animated.Value(-45) : new Animated.Value(-10))[0]
})

useEffect(() => {
colors.forEach((c, index) => {
let value = focusedTab === index ? 1 : 0
Animated.timing(c, {
toValue: value,
duration: 200,
useNativeDriver: false
}).start()
})
iconSizes.forEach((s, index) => {
let value = focusedTab === index ? 40 : 28
Animated.timing(s, {
toValue: value,
duration: 200,
useNativeDriver: false
}).start()
})
boxSizes.forEach((s, index) => {
let value = focusedTab === index ? 70 : 50
Animated.timing(s, {
toValue: value,
duration: 200,
useNativeDriver: false
}).start()
})
topMargins.forEach((s, index) => {
let value = focusedTab === index ? -45 : -10
Animated.timing(s, {
toValue: value,
duration: 200,
useNativeDriver: false
}).start()
})
}, [focusedTab])

const bgColorAnimation = (c) => c.interpolate({
inputRange: [0, 1],
outputRange: ["rgb(74,74,74)", "rgb(82,224,84)"]
})

let [fontsLoaded] = useFonts({
'MyCustomIconSet': MyCustomIconSet,
});

const icons = {
carrot: "", // 
tea: "", // 
coffee: "", // 
pineapple: "", // 
cherries: "", // 
}

if (!fontsLoaded) {
return <AppLoading/>;
} else {

const TabWrapper = styled.View`
display: flex;
flex: 1;
flex-direction: row;
align-self: stretch;
justify-content: center;
align-content: center;
`

const IconWrapper = styled(Animated.View)`
position: relative;
width: 70px;
height: 70px;
justify-content: center;
margin-top: -10px;
border-radius: 35px;
`

const Icon = styled(Animated.Text)`
font-family: "MyCustomIconSet";
color: white;
padding: 10px;
text-align: center;
color: ${props => props.color};
`

const TabBarBg = styled.View`
background-color: #eaeaea;
display: flex;
flex: 1;
`

const pages = [
{
title: "Carrot Screen",
content: "Carrots are good for you!",
icon: icons.carrot
},
{
title: "Cherry Screen",
content: "Who doesn't love cherries?",
icon: icons.cherries
},
{
title: "Coffee Screen",
content: "Coffee jumpstarts your coding!",
icon: icons.coffee
},
{
title: "Pineapple Screen",
content: "Pine and apple? No thanks...",
icon: icons.pineapple
},
{
title: "Tea Screen",
content: "Milk or lemon?",
icon: icons.tea
}
]

return <Tab.Navigator screenOptions={{
headerShown: false,
tabBarShowLabel: false,
tabBarStyle: {position: 'absolute'},
tabBarBackground: () => (
<TabBarBg/>
),
}}>
{pages.map((page, index) => (
<Tab.Screen key={"tab-"+index} name={page.title}
options={{
tabBarIcon: ({focused}) => <TabWrapper>
<IconWrapper style={{
backgroundColor: bgColorAnimation(colors[index]),
marginTop: topMargins[index],
width: boxSizes[index],
height: boxSizes[index]
}}>
<Icon style={{fontSize: iconSizes[index]}}
color={focused ? "black" : "white"}>{page.icon}</Icon>
</IconWrapper>
</TabWrapper>
}}
listeners={{
tabPress: e => {
setFocusedTab(index)
},
}}>
{props => <Screen {...props} title={page.title} content={page.content}/>}
</Tab.Screen>)
)}
</Tab.Navigator>
}
}

export default BottomTabNavigatorArticle

1. Loading your custom icon font

To make the tabs look pretty, let’s load the icon font that will be used to display the tab icons. If you are interested in how to create one, look here.
To follow along, grab the file ot the entire repo.

Once the icon font is loaded, let’s map the icons to an object, so they can be referred by name rather than html entity.

2. Creating the navigator and loading the pages

Constructing the tab navigation takes a couple of elements. There is needed some page data, the element itself and some styling that includes icon mapping and a set of styled components in our case. Once it is all in place, we can start animating the elements!

The Static page data
We are going to need some sort of a static page data that we can loop through and pass down to the generic page screen component we have created earlier.

Static Styles
We are using styled components those we are looking to animate in the next step. At first, let’s use static components, so at least we have something to look at.

The Navigation Element

Once the is page data, navigator and icons are in place, we are finally ready to create a static bottom tab navigation as per the documentation.

3. Creating the animations

We know the following:

  • It is possible to use a custom element as the tab component as well as it is entirely possible to style it the way we want.
  • It is possible to listen for a focused event and apply styles conditionally based on their focused state.

So let’s turn our focus on creating the animations themselves without worrying too much about how they will be applied to the appropriate elements.

If you are not familiar with the React Native Animation Library, then let me recommend you reading through the docs first.

Basically the way it works is that you can specify the end values and the transition will be calculated for you without interfering with the main thread. This way the animation is smooth and not blocking other parts of the ui.

Mass creation

The little issue is that one of these animation methods need to be applied to all tab elements individually per type of animation.

So if you are looking to animate the size and position of all five elements, you need to create five size and another five position animations. To create them manually would lead to lots of typing, lots of repetition and lots of possibility of errors not to mention maintainability.

Si what I have decided to do was to use loops to mass produce the animation objects.

Mass creating animations

Once app loads, we can start listening for the focused state value that gets updated at selecting an item. When it changes, the animations need to be set accordingly. (send inactive elements to the start(not focused) position and the selected one to the end.

Color Interpolation

The above approach is enough to animate the size and position, but what about colour?

Well yes, that is a bit trickier but the Animated library does have support for it too. Check out the interpolate method docs here.

Our function to calculate the interim colours looks like this:

So now, similarly to the others, we can mass create the animations and run them as needed.

Animated Styles

At last let’s apply the animations to the elements.

In order to do this, first you need to change the element itself to its animated equivalent. So View becomes Animated.View, Text becomes Animated.Text.
Since we are using styled components, the change affects them only.

If you are familiar with styled components, you might be tempted to apply the animated styles through props, however let me save you a few minutes here. It will not work. At the time of writing, there is no support for animated props in styled components. This thread was closed years ago without a resolution, so it is probably safe to assume that this feature is not in the pipeline.

Still there is a solution and it doesn’t look too bad. You can apply the animated styled directly to the element.

I would definitely argue that styled props would look better, but this is what we have.

Conclusion

There you have it! This is how you animate the bottom tabs in React Native. I hope you have enjoyed this tutorial. I certainly did enjoy writing it!

Stay tuned as I have plenty similar neat little tricks in the works! Have a lovely day!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store