
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.
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:
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!
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.
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:
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.
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',
},
});
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:
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.
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:
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.
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.


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:

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.
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>
);
};
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,
}}
/>
);
};

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!
Originally published:
November 2, 2021