Connect with a Development Expert

Get started with Xperts

How to Create an Animated TabBar in React Native

By Xperts

No items found.

When Coinbase moved over to React Native from React at the beginning of 2021, it wasn't a decision that they made lightly, nor did it happen overnight. At the time, they provided service to over 56 million users, so it was important that whatever changes they made would not lead to a regression in features or lackluster performance as it could have serious implications for their customers, not to mention their business.

The size of our native codebases was also notable. Migrating to React Native meant reimplementing over 200 screens, many of which contained substantial business logic. The transition also involved retraining our 30+ native engineers while continuing to make progress building new features and sunsetting our legacy apps. There were many moving pieces, but we were able to deliver significant product performance improvements at each stage of the migration.

The app migration to React Native required them to reimplement over 200 screens. Other than that, there were many movies pieces, and considering their large user base (over 56 million users), they were able to deliver performance improvements as well.

One of the highlights of their migrated app is the animated tab bar. In this tutorial, let's learn how to build an animated collapsible header with a tab bar in React Native. It will scroll on the position of the FlatList component. We will go through the basics of creating a new Animated value as well as explaining the significance of functions and properties like interpolation, extrapolate, and so on.

Prerequisites

To follow along with this tutorial, please make sure that you are 1) familiarized with JavaScript/ES6, 2) understand the basics of React Native, and 3) are able to meet the following requirements in your local dev environment:

  • You have Node.js version 14.x.x or above installed
  • You have access to a package manager like npm, yarn, or npx
  • You have react-native-cli installed, or you are using npx

Depending on your skill level and experience, it may also be beneficial to brush up on how to scaffold a new custom mobile app with Crowdbotics prior to jumping into this tutorial.

Note, the example app is created using React Native version 0.65.

Now that we've gotten all of that out of the way let's get started!

Setting Up a React Native App

Begin by creating a new React Native project. To initialize one, open up a terminal window and run the command:

npx react-native init MyProjectName

# navigate inside your project directory
cd MyProjectName

Where MyProjectName is the name of your app. After the project directory is created, let's install react-native-tab-view library. This library will help us create a top Tab Bar that will cross-platform (that is, both on iOS and Android). The installation process of this library also includes installing a couple of supporting libraries.

Open the terminal window and run the command:

yarn add react-native-tab-view react-native-pager-view

Let's also install another library that will help us calculate the safe area inset of a device. It's called react-native-safe-area-context. It will allow the app screen's content to position appropriately and do not hide behind a device's notch or status bar.

yarn add react-native-safe-area-context

If you are on mac OS and developing for iOS devices, you will need to install iOS native dependencies for the libraries we've just installed. In the terminal window, run the following command:

npx pod-install

After installing these dependencies, you will have to build the app for iOS or Android to see it in action either on a device or a simulator.

To build the app for iOS, from the terminal window, run: yarn ios. To build the app for Android, from the terminal window, run: yarn android.

Create a Directory Structure

Now, let's create a basic directory structure. Start by creating an src/ directory at the root of your React Native project.

Inside it, create the following directories:

  • components/ where all the re-usable components are stored.
  • screens/ where we will create a BooksScreen.js, the main screen component.
  • data/ where we will store mock data arrays.

Adding Mock Data

For the purpose of the example app we're creating in this tutorial, let's use some mock data. Feel free to skip this step if you are planning to use your own data set though!

Inside the data/ directory, create a new file called books.js and add the code snippet from this GitHub gist file.

Similarly, create another file called authors.js and add the code snippet from this GitHub gist file.

We will use data from each of these files to display a list for each tab in the BooksScreen.js file.

Creating a Header

Let's start by creating a Header component. Inside the components/ directory, create a new file called Header.js.

The Header component is View with a fixed height that displays a Text. It will receive two props from the BooksScreen. The first prop is going to be the actual height of the header. The second prop is the title to display in the Header component.

Add the following code snippet:

// components/Header.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

export const Header = ({ title, headerHeight }) => {
 return (
   <View
     style={[
       styles.headerContainer,
       {
         height: headerHeight,
       },
     ]}><Text style={styles.headerText}>{title}</Text></View>
 );
};

const styles = StyleSheet.create({
 headerContainer: {
   top: 0,
   zIndex: 10,
   position: 'absolute',
   backgroundColor: '#FE8F8F',
   width: '100%',
   alignItems: 'center',
   justifyContent: 'center',
 },
 headerText: {
   fontSize: 32,
   fontWeight: '600',
   color: '#fff',
 },
});

Creating a Custom Tab Bar with react-native-tab-view

List and List Item Re-usable Components

The Tab Bar user experience has become common among mobile apps. One app that comes to mind is Twitter. In this section, let's create a custom tab bar using the react-native-tab-view library to mimic this behavior.

It will be displayed after the Header in the BookScreen. There will be two tabs. The first tab will display a list of books, and the second tab will display a list of authors.

The react-native-tab-view library brings the following features to the app without the need to write a single line of code these features:

  • Scrollable tabs
  • Smooth animations and gestures when switching from one to another tab
  • Customization

Due to its nature of being "highly customizable", we will have to tweak it to render the content inside each tab bar. Start by creating the following files inside the components/ directory.

  • Scene.js
  • CustomTabBar.js
  • TabItem.js

Let's start with the most re-usable component, TabItem. This component will display the Image and the title of a book or an author in both tabs. It receives the item object as the only prop. This object will contain all the data fields from the mock array.

The Open TabItem.js and inside it add the following code:

// components/TabItem.js
import React from 'react';
import { View, Image, Text, StyleSheet } from 'react-native';

export const TabItem = ({ item }) => {
 return (
   <View style={styles.container}><Image source={{ uri: item.image_url }} style={styles.image} /><Text style={styles.text}>{item.title}</Text></View>
 );
};

const styles = StyleSheet.create({
 container: {
   flexDirection: 'row',
 },
 image: {
   width: 100,
   height: 140,
   borderRadius: 8,
 },
 text: {
   fontWeight: '600',
   fontSize: 20,
   marginHorizontal: 16,
   flex: 1,
 },
});

Next, let's create a Scene component. This component is a FlatList from React Native. It will display all items in the form of a scrollable, vertical list. It receives four props:

  • data: array of items
  • renderItem: the function to render each item in the list. We will handle this inside CustomTabBar for convenience.
  • headerHeight: the height of the header on the app screen.
  • tabBarHeight: the height of the tabs bar.

Open Scene.js and add the following code snippet:

// components/Scene.js
import React from 'react';
import { FlatList, View, StyleSheet } from 'react-native';

export const Scene = ({ data, renderItem, headerHeight, tabBarHeight }) => {
 const renderSpace = () =><View style={styles.spaceContainer} />;

 return (
   <FlatList
     data={data}
     keyExtractor={(item, index) => index.toString()}
     renderItem={renderItem}
     ItemSeparatorComponent={renderSpace}
     ListHeaderComponent={renderSpace}
     ListFooterComponent={renderSpace}
     showsVerticalScrollIndicator={false}
     contentContainerStyle={{
       paddingTop: headerHeight + tabBarHeight,
       paddingHorizontal: 10,
     }}
   />
 );
};

const styles = StyleSheet.create({
 spaceContainer: {
   height: 20,
 },
});

The Scene component will render below the Tab Bar.

Creating the Custom TabBar

The CustomTabBar component will display the tab bar with two tabs and then render the data for each tab below the tab bar. The data for each tab will render using the re-usable component Scene.

Open the CustomTabBar.js file and add the following code:

// components/CustomTabBar
import React, { useState } from 'react';
import { View, Text, StyleSheet, useWindowDimensions } from 'react-native';
import { TabBar, TabView } from 'react-native-tab-view';

import { Scene } from './Scene';
import { TabItem } from './TabItem';

export const CustomTabs = ({
 routes,
 booksData,
 authorsData,
 tabBarHeight,
 headerHeight,
}) => {
 const [tabIndex, setTabIndex] = useState(0);
 const layout = useWindowDimensions();

 const renderTabOne = ({ item, index }) => {
   return <TabItem item={item} />;
 };

 const renderTabTwo = ({ item, index }) => {
   return <TabItem item={item} />;
 };

 const renderScene = ({ route }) => {
   let data;
   let renderItem;
   switch (route.key) {
     case 'tab1':
       data = booksData;
       renderItem = renderTabOne;
       break;
     case 'tab2':
       data = authorsData;
       renderItem = renderTabTwo;
       break;
     default:
       return null;
   }

   return (
     <Scene
       data={data}
       renderItem={renderItem}
       headerHeight={headerHeight}
       tabBarHeight={tabBarHeight}
     />
   );
 };

 const renderLabel = ({ route, focused }) => (
   <Text style={[styles.label, { opacity: focused ? 1 : 0.5 }]}>
     {route.title}
   </Text>
 );

 const renderTabBar = props => (
   <View style={styles.container}><TabBar
       {...props}
       style={styles.tab}
       renderLabel={renderLabel}
       indicatorStyle={styles.indicator}
     /></View>
 );

 return (
   <TabView
     onIndexChange={index => setTabIndex(index)}
     navigationState={{ index: tabIndex, routes }}
     renderScene={renderScene}
     renderTabBar={renderTabBar}
     initialLayout={{ width: layout.width }}
   />
 );
};

const styles = StyleSheet.create({
 container: {
   top: 200,
   left: 0,
   right: 0,
   zIndex: 10,
   position: 'absolute',
   width: '100%',
 },
 tab: {
   backgroundColor: '#FE8F8F',
 },
 indicator: {
   backgroundColor: '#FF0000',
 },
 label: {
   fontSize: 20,
   color: '#fff',
   fontWeight: '700',
 },
});

In the above code snippet, the TabView is the container component from the react-native-tab-view library. It is responsible for rendering and managing tabs. It detects which tab is currently active by using a tab's index value, thus, using the tabIndex state variable on the prop onIndexChange.

The navigationState prop on the TabView component contains the state of the tab. Its property index represents the index of the active route in the routes array. The CustomTabBar receives the routes array as a prop from the BooksScreen component.

The renderScene prop accepts a callback function that returns a React Native component to display the contents of a tab. In our case, the TabItem.

The renderTabBar prop accepts a callback function that returns a custom React Native component to display the tab bar using <TabBar/> component from react-native-tab-view.

The <TabBar/> further has props like renderLabel and indicatorStyle to display the tab label and modify the styles for the tab indicator. The tab indicator is shown on the active tab.

try our app estimate calculator CTA image

Creating BooksScreen Component

The BooksScreen component is the main screen file for our example app.

// screens/BooksScreen.js
import React, { useState } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

// Custom Components
import { CustomTabs } from '../components/CustomTabBar';
import { Header } from '../components/Header';

// Mock data files
import { BOOKS } from '../data/books';
import { AUTHORS } from '../data/authors';

// Screen Level Constants
const HEADER_HEIGHT = 200;
const TAB_BAR_HEIGHT = 48;

export const BooksScreen = () => {
 const insets = useSafeAreaInsets();

 // routes for each tab is defined here:
 const [routes] = useState([
   { key: 'tab1', title: 'Books' },
   { key: 'tab2', title: 'Authors' },
 ]);

 return (
   <View style={styles.container}><Header
       title="What are you reading today?"
       headerHeight={HEADER_HEIGHT}
     /><CustomTabs
       routes={routes}
       booksData={BOOKS}
       authorsData={AUTHORS}
       headerHeight={HEADER_HEIGHT}
       tabBarHeight={TAB_BAR_HEIGHT}
     /></View>
 );
};

const styles = StyleSheet.create({
 container: {
   flex: 1,
 },
});

Also, modify the App.js file and add the following code snippet:

import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import { BooksScreen } from './src/screens/BooksScreen';

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

export default App;

Now, let's see the progress with the example app made so far.

Run the build command for your preferred platform (iOS or Android). You will get the following result:

Animating Header and Custom TabBar

Header and the CustomTabs components both should scroll up when a list is scrolled up in any of the tabs. You will need to animate both these components.

Start by modifying the import statements BooksScreen.js file. You will need Animated from React Native and useRef hook from React.

// screens/BooksScreen.js
import React, { useState, useRef } from 'react';
import { View, StyleSheet, Animated } from 'react-native';

The scroll position will have an Animated.Value of 0. To create collapsible animation, Animated.Value is required. Then, define a variable called scrollY with a new Animated.Value inside the BooksScreen component.

The value of scrollY and insets.top will be passed as props to both the Header and CustomTabs components.

export const BooksScreen = () => {
 const insets = useSafeAreaInsets();
 // Define scrollY
 const scrollY = useRef(new Animated.Value(0)).current;
 const [routes] = useState([
   { key: 'tab1', title: 'Books' },
   { key: 'tab2', title: 'Authors' },
 ]);

 return (
   <View style={styles.container}><Header
       title="What are you reading today?"
       headerHeight={HEADER_HEIGHT}
       // Add below
       scrollY={scrollY}
       topInset={insets.top}
     /><CustomTabs
       routes={routes}
       booksData={BOOKS}
       authorsData={AUTHORS}
       headerHeight={HEADER_HEIGHT}
       tabBarHeight={TAB_BAR_HEIGHT}
       // Add below
       scrollY={scrollY}
       topInset={insets.top}
     /></View>
 );
};

The useRef hook allows modifying the Animated value on scroll. It provides a current property that is persisted throughout a component's lifecycle.

Modify Header Component to Make it Collapsible

First, let's update the Header component file to make it animatable. To animate the height of the header view on the scroll, you will use interpolation. The interpolate() function on Animated.Value allows an input range to map to a different output range.

In the current scenario, when the user scrolls, the interpolation on Animated.Value will change the scale of the Header to slide to the top on scroll along the y-axis. This effect will minimize the initial value of the height of Animated.View.

The interpolation must specify an extrapolate value. This determines the scaling of the Header's height to be visible at the last value in outputRange. There are three different values for extrapolate available, but we are going to use clamp.

The inputRange will be 0 to the headerHeight. The outputRange will be from 0 to -headerHeight plus the top inset. This will be applied on the translateY style property in the Animated.View. It will interpolate the value of scrollY.

There is also a Text component inside the Header. We will change the opacity of the text depending on the scroll position.

Modify the code snippet in Header.js file:

// components/Header.js

import React from 'react';
// Make sure to import Animated
import { StyleSheet, Animated } from 'react-native';

export const Header = ({ title, headerHeight, topInset, scrollY }) => {
 const interpolatedHeight = scrollY.interpolate({
   inputRange: [0, headerHeight],
   outputRange: [0, -headerHeight + topInset],
   extrapolate: 'clamp',
 });

 const opacity = scrollY.interpolate({
   inputRange: [0, headerHeight],
   outputRange: [1, 0],
   extrapolateRight: 'clamp',
 });

 return (
   <Animated.View
     style={[
       styles.headerContainer,
       {
         height: headerHeight,
         transform: [{ translateY: interpolatedHeight }],
       },
     ]}><Animated.Text style={[styles.headerText, { opacity }]}>
       {title}
     </Animated.Text></Animated.View>
 );
};

Modify the CustomTabs Component

In CustomTabs.js file, start by updating the import statements as below:

// components/CustomTabs

import React, { useState, useEffect, useRef } from 'react';
import { Text, StyleSheet, useWindowDimensions, Animated } from 'react-native';

Then, update the props for CustomTabs component and also define these three reference values:

export const CustomTabs = ({
 routes,
 booksData,
 authorsData,
 tabBarHeight,
 headerHeight,
 scrollY,
 topInset,
}) => {
 const [tabIndex, setTabIndex] = useState(0);
 const layout = useWindowDimensions();
 let listRefArr = useRef([]);
 let scrollOffset = useRef({});
 let isListGliding = useRef(false);

 // ...
};

Next, add a useEffect hook. Inside it, we will add an event listen using scrollY.addListener that will maintain the same scrollOffset individual tab's index. This will keep track of the tab scrolled position.

useEffect(() => {
 scrollY.addListener(({ value }) => {
   const currentRoute = routes[tabIndex].key;
   scrollOffset.current[currentRoute] = value;
 });
 return () => {
   scrollY.removeAllListeners();
 };
}, [routes, tabIndex]);

Add a handler method called syncScrollOffset. It will invoke to synchronize scroll offsets on onMomentumScrollEnd and onScrollEndDrag events. It will iterate through the inactive tab and update their scroll offset depending on the scrolled position of Header.

const syncScrollOffset = () => {
 const currentRouteKey = routes[tabIndex].key;
 listRefArr.current.forEach(item => {
   if (item.key !== currentRouteKey) {
     if (scrollY._value < headerHeight && scrollY._value >= 0) {
       if (item.value) {
         item.value.scrollToOffset({
           offset: scrollY._value,
           animated: false,
         });
         scrollOffset.current[item.key] = scrollY._value;
       }
     } else if (scrollY._value >= headerHeight) {
       if (
         scrollOffset.current[item.key] < headerHeight ||
         scrollOffset.current[item.key] == null
       ) {
         if (item.value) {
           item.value.scrollToOffset({
             offset: headerHeight,
             animated: false,
           });
           scrollOffset.current[item.key] = headerHeight;
         }
       }
     }
   }
 });
};

const onMomentumScrollBegin = () => {
 isListGliding.current = true;
};

const onMomentumScrollEnd = () => {
 isListGliding.current = false;
 syncScrollOffset();
};

const onScrollEndDrag = () => {
 syncScrollOffset();
};

Next, pass the custom methods from above code snippet to invoke events such as onMomentumScrollBegin,onMomentumScrollEnd and onScrollEndDrag on the Animated.FlatList inside the Scene component.

<Scene
 data={data}
 renderItem={renderItem}
 headerHeight={headerHeight}
 tabBarHeight={tabBarHeight}
 scrollY={scrollY}
 onMomentumScrollBegin={onMomentumScrollBegin}
 onScrollEndDrag={onScrollEndDrag}
 onMomentumScrollEnd={onMomentumScrollEnd}
 onScrollRef={ref => {
   if (ref) {
     const found = listRefArr.current.find(e => e.key === route.key);
     if (!found) {
       listRefArr.current.push({
         key: route.key,
         value: ref,
       });
     }
   }
 }}
/>

Also, modify the renderTabBar function to change its scroll position based on the Animated.Value along the y-axis.

const renderTabBar = props => (
 <Animated.View
   style={[
     styles.container,
     {
       transform: [
         {
           translateY: scrollY.interpolate({
             inputRange: [0, headerHeight],
             outputRange: [headerHeight, topInset],
             extrapolate: 'clamp',
           }),
         },
       ],
     },
   ]}><TabBar
     {...props}
     style={styles.tab}
     renderLabel={renderLabel}
     indicatorStyle={styles.indicator}
   /></Animated.View>
);

Also, remove the value of top in the CustomTabs style. We previously defined this to position the tab below the Header component.

const styles = StyleSheet.create({
 container: {
   // top: 200, <-- remove this or comment it out
   left: 0,
   right: 0,
   zIndex: 10,
   position: 'absolute',
   width: '100%',
 },
 // ... rest remains unchanged
}

Next, modify the Scene.js file:

import React from 'react';
import { Animated, View, StyleSheet } from 'react-native';

export const Scene = ({
 data,
 renderItem,
 headerHeight,
 tabBarHeight,
 onScrollRef,
 onMomentumScrollBegin,
 onScrollEndDrag,
 onMomentumScrollEnd,
 scrollY,
}) => {
 const renderSpace = () =><View style={styles.spaceContainer} />;

 return (
   <Animated.FlatList
     scrollToOverflowEnabled={true}
     scrollEventThrottle={16}
     onScroll={Animated.event(
       [{ nativeEvent: { contentOffset: { y: scrollY } } }],
       {
         useNativeDriver: true,
       },
     )}
     ref={onScrollRef}
     data={data}
     keyExtractor={(item, index) => index.toString()}
     renderItem={renderItem}
     onMomentumScrollBegin={onMomentumScrollBegin}
     onScrollEndDrag={onScrollEndDrag}
     onMomentumScrollEnd={onMomentumScrollEnd}
     ItemSeparatorComponent={renderSpace}
     ListHeaderComponent={renderSpace}
     ListFooterComponent={renderSpace}
     showsVerticalScrollIndicator={false}
     contentContainerStyle={{
       paddingTop: headerHeight + tabBarHeight,
       paddingHorizontal: 10,
     }}
   />
 );
};

Conclusion

If you are trying the Animated library from React Native for the first time, wrapping your head around it might take a bit of time, and that's part of the process. Investing deep in the UX patterns in your app can help improve the experience of your app user completely.

Here is the link to the complete source code on a GitHub repo.

Have you checked out the Crowdbotics App Builder yet? Designed to facilitate the rapid development of universal software applications, it allows developers to scaffold and deploy working apps quickly by identifying the best packages for a given feature set.

Consider Crowdbotics your all-in-one dashboard for app development! Do you have a project that you're not sure where to start? We can help—our expert team of PMs, designers, developers, and engineers are here to help you go from idea to deployment 3x faster than manual development. To learn more and get a detailed estimate, get in touch with us today!

No items found.

Originally published:

November 2, 2021

Related Articles

No items found.
No items found.

© 2026 Xperts. All rights reserved.