A ScrollView
is a component that enables to view the content on a device's screen that is not able to be displayed in one screen. Using a scroll view component, the content can either be scrolled vertically or horizontally. This depends a lot on the design of the mobile application.
In React Native, to implement a scroll view, there are two types of components available: ScrollView
and FlatList
. The ScrollView
component renders all children at once. This is useful if the data to display is static or there aren't too many data items in the list. The FlatList
component is performant and optimal for displaying a huge scrollable list of data items.
For example, this how a ScrollView
component is implemented in a React Native app:
<ScrollView style={{ backgroundColor: 'white', marginHorizontal: 20 }}>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</Text>
</ScrollView>
Both of these scrollable components have at least one thing in common: a scroll bar indicator. By default, the scroll bar indicator is visible whether the content is displayed horizontally or vertically. To disable this vertical scroll bar indicator you would add the prop showsVerticalScrollIndicator
with a boolean value of false:
<ScrollView style={{ backgroundColor: 'white', marginHorizontal: 20 }} showsVerticalScrollIndicator={false}>
However, the implementation of this scroll bar indicator is not directly customizable on cross-platforms in React Native. If you are building an app whose screen design depends on displaying a customized scroll bar indicator, then let's build one in this tutorial. To implement this, we are going to use React Native Animated API.
The source code is available at GitHub.
Prerequisites
To follow this tutorial, please make sure you are familiarized with JavaScript/ES6 and meet the following requirements in your local dev environment:
-
Node.js version >=
12.x.x
installed. - Have access to one package manager such as npm or yarn or npx.
- Have a basic understanding of Redux store, actions, and reducers.
- expo-cli installed, or use npx.
The example in the following tutorial is based on Expo SDK 39
.
Do note that all the code mentioned in this tutorial works with the vanilla React Native project as well.
Create a new React Native project with expo-cli
To create a new React Native project using expo-cli
, execute the following command from a terminal window:
npx expo init custom-scroll-indicator
# navigate into that directory
cd custom-scroll-indicator
And that's it. We are not using any third party library but the approach discussed in this post is easily integrated with any other libraries that your React Native app depends on.
Before we move onto the next section, let's start creating a mock screen. Open App.js
file and add the following code snippet:
import React, { useState, useRef } from 'react';
import { ScrollView, Text, View, Animated } from 'react-native';
import { StatusBar } from 'expo-status-bar';
export default function App() {
return (
<>
<StatusBar style="light" />
<View style={{ flex: 1, backgroundColor: '#892cdc', paddingTop: 50 }}>
<View style={{ alignItems: 'center' }}>
<Text style={{ color: 'white', fontSize: 24, fontWeight: '700' }}>
Custom Scroll Bar
</Text>
</View>
</View>
</>
);
}
To see the output of this step, please go back to the terminal window execute one of the following commands depending on the OS (whether iOS or Android) of the simulator or the real device the Expo Client app is running:
# trigger expo development server
yarn start
# for iOS
yarn run ios
# for android
yarn run android
When the app is up and running, here is the output you are going to get:
Add mock data
Inside the scroll view component, we are going to display some mock data. Let's add it to the React Native project. Create a new directory called constants/
and inside it a new file called data.js
.
This file is going to contain an object called booksData
that has two properties:
-
title
of the book item. -
description
is the long form of the text where the custom scroll bar is going to be used to scroll the text inside theScrollView
component.
Add the following code snippet to this file:
export const booksData = {
title: 'The Hunger Games',
description:
'Winning will make you famous. Losing means certain death. The nation of Panem, formed from a post-apocalyptic North America, is a country that consists of a wealthy Capitol region surrounded by 12 poorer districts. Early in its history, a rebellion led by a 13th district against the Capitol resulted in its destruction and the creation of an annual televised event known as the Hunger Games. In punishment, and as a reminder of the power and grace of the Capitol, each district must yield one boy and one girl between the ages of 12 and 18 through a lottery system to participate in the games. The tributes are chosen during the annual Reaping and are forced to fight to the death, leaving only one survivor to claim victory. When 16-year-old Katniss young sister, Prim, is selected as District 12 female representative, Katniss volunteers to take her place.'
};
Make sure to import object inside the App.js
file after other import statements.
// ...
import { booksData } from './constants/data';
Display mock data using a ScrollView
The mock data we created in the previous section is going to be displayed inside a ScrollView
component. The content inside this scroll view is displayed with two Text
components. One to display the title of the book item and another to display the description.
This ScrollView
component is not going to take the whole screen to display the content. Thus, the default scroll bar indicator is shown when the description is scrolled. We are going to add an empty View
after the ScrollView
component with a value of flex: 4
such that this empty view takes slightly more than half of the screen.
There is also a View
component that wraps the ScrollView
. For now, it adds horizontal padding but later will be crucial to display the custom scroll bar indicator next to the ScrollView
component. Thus, let's add the flexDirection: 'row'
property to this wrapper View
component.
Modify the App.js
file and add the following JSX:
export default function App() {
return (
<>
<StatusBar style="light" />
<View style={{ flex: 1, backgroundColor: '#892cdc', paddingTop: 50 }}>
<View style={{ alignItems: 'center' }}>
<Text style={{ color: 'white', fontSize: 28, fontWeight: '700' }}>
Custom Scroll Bar
</Text>
</View>
<View style={{ flex: 3, marginVertical: 20 }}>
<View
style={{ flex: 1, flexDirection: 'row', paddingHorizontal: 20 }}>
<ScrollView>
<Text
style={{
fontSize: 22,
color: 'white',
fontWeight: '600',
marginBottom: 12
}}>
{booksData.title}
</Text>
<Text
style={{
fontSize: 18,
color: 'white'
}}>
{booksData.description}
</Text>
</ScrollView>
</View>
</View>
<View style={{ flex: 4 }} />
</View>
</>
);
}
Output after this step:
Hide the default scroll indicator by adding the showsVerticalScrollIndicator
prop to the ScrollView
component. Also, add the contentContainerStyle
prop with a to apply paddingRight
to its children (which are the content being displayed and custom scroll bar we have to create).
<ScrollView
contentContainerStyle={{ paddingRight: 14 }}
showsVerticalScrollIndicator={false}
>
Create the custom scroll bar
Next, to the content displayed, let's add a scroll bar. Add a View
component whose height
is set to 100%
. This will display the scroll bar with as much height as the height of its parent container.
<View style={{ flex: 1, flexDirection: 'row', paddingHorizontal: 20 }}>
{/* ScrollView component here */}
<View
style={{
height: '100%',
width: 6,
backgroundColor: '#52057b',
borderRadius: 8
}}></View>
</View>
The width
in the above code snippet can be customized with the value you can provide.
The output of this step:
Create the custom scroll bar indicator
To display a custom scroll bar indicator, we need to calculate the size of the scroll bar indicator first. This can be done by comparing the complete height of the scroll bar and the visible height of the scroll bar that is the indicator.
In the App
component, define two state variables using the useState
hook and a new variable where we store the size of the bar indicator.
const [completeScrollBarHeight, setCompleteScrollBarHeight] = useState(1);
const [visibleScrollBarHeight, setVisibleScrollBarHeight] = useState(0);
const scrollIndicatorSize =
completeScrollBarHeight > visibleScrollBarHeight
? (visibleScrollBarHeight * visibleScrollBarHeight) /
completeScrollBarHeight
: visibleScrollBarHeight;
Next, create the scroll bar indicator inside the scroll bar. The indicator is going to have its height equivalent to the scrollIndicatorSize
.
// ScrollView component here
<View
style={{
height: '100%',
width: 6,
backgroundColor: '#52057b',
borderRadius: 8
}}>
<View
style={{
width: 6,
borderRadius: 8,
backgroundColor: '#bc6ff1',
height: scrollIndicatorSize
}}
/>
</View>
The scroll bar indicator is now displayed:
To change the position of this indicator, we have to animate its value.
Animate the scroll bar indicator
We are going to animate the position of the scroll bar indicator as the content inside the ScrollView
is scrolled. To create an animation, Animated.Value
is required. Define the scrollIndicator
variable with an Animated.Value
of 0
.
Add the following code snippet after state variables are declared in App
component:
const scrollIndicator = useRef(new Animated.Value(0)).current;
Then define a variable called difference
that is used to calculate the height of the scroll bar indicator if it is greater than the size of the scroll indicator. This value is used to calculate the range of interpolation to change the position of the scroll bar indicator to move along the y-axis.
To change the position of the scroll bar indicator, we use the Animated.multiply
method. This method creates a new Animated value that is composed from two values multiplied together. This new value is what the change in the position of the scroll bar indicator is going to be when the content is scrolled in the ScrollView
. To change the position, we need to multiply the current value of the scrollIndicator
and the visible height of the scroll bar indicator divided by the complete height of the scroll bar.
After getting the new Animate value, interpolation is applied. This is done by using the interpolate()
function on the new Animated value and it allows an input range to map to an output range.
The interpolation must specify an extrapolate
value. There are three different values for extrapolate
available, but we are going to use clamp
. It prevents the output value from exceeding the outputRange
.
Add the following code snippet in the App
component:
const difference =
visibleScrollBarHeight > scrollIndicatorSize
? visibleScrollBarHeight - scrollIndicatorSize
: 1;
const scrollIndicatorPosition = Animated.multiply(
scrollIndicator,
visibleScrollBarHeight / completeScrollBarHeight
).interpolate({
inputRange: [0, difference],
outputRange: [0, difference],
extrapolate: 'clamp'
});
Then, convert the View
component that displays the scroll bar indicator into an Animated.View
. We are going to add a prop called transform
. It is going to change the position of the scroll bar indicator.
The value of this prop is going to be an array and inside it, a transformation object is defined. This object specifies the property that is transformed, as the key and its value is going to be the scrollIndicatorPosition
.
<Animated.View
style={{
width: 6,
borderRadius: 8,
backgroundColor: '#bc6ff1',
height: scrollIndicatorSize,
transform: [{ translateY: scrollIndicatorHeight }]
}}
/>
Next, we need to set the height of the scroll bar and scroll bar indicator that is visible when the content inside the ScrollView
changes. For this, there are two props used in combination:
-
onContentSizeChange
whose value is a handler function with the width and the height of the content. For our demo, we are going to use the height of the content to update the height of the complete scroll bar. -
onLayout
is used to update the height of the visible scroll bar.
To animate the scroll bar indicator's position when the height of the content changes another prop called onScroll
is used. It accepts an Animated.event()
as the value which is used to handle gestures like panning and in our case, scrolling. The frequency of the scrolling event is controlled using a prop called scrollEventThrottle
. It controls how often the scroll event will be fired while scrolling.
Modify the props of ScrollView
component as shown below:
<ScrollView
contentContainerStyle={{ paddingRight: 14 }}
showsVerticalScrollIndicator={false}
onContentSizeChange={height => {
setCompleteScrollBarHeight(height);
}}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => {
setVisibleScrollBarHeight(height);
}}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollIndicator } } }],
{ useNativeDriver: false }
)}
scrollEventThrottle={16}>
{/* Rest remains same */}
</ScrollView>
Here is the output after this step on an iOS simulator:
Here is the output after this step on an Android device:
Conclusion
I hope you had fun reading this tutorial. 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 practice and that's part of the process.
Here is another post I wrote on React Native Animated API.
Top comments (5)
Nice Explanation. Do you have any tut for animations in React Native ?
Thanks! By tut you mean a video or an in-detail post?
The link in the end to another post, is in-detail post I wrote about how to apply basics of React Native Animated API such as interpolation, extrapolate and so on. I am still trying to write a completely beginner friendly post that contain simple use cases for almost the most part of the API.
Ya, that would help! thanks 🤟🏻
Nice post!
Thank you, Eneas :)