Skip to content

How to Animate a Header View on Scroll With React Native Animated

Published:

9 min read

cover_image

The Animated library from React Native provides a great way to add animations and give app users a smooth and friendlier experience.

In this tutorial, let’s explore a way to create a header view component that animates on the scroll position of the ScrollView component from React Native. 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, contentOffset, and so on.

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 on your local dev environment.

The example in the following tutorial is based on Expo SDK 38.

Installing dependencies

Start by creating a new React Native app generated with expo-cli. Do note that all the code mentioned in this tutorial works with plain React Native apps as well. Open up a terminal window and execute the following command:

npx expo-cli init animate-header-example

# after the project is created, navigate into the directory
cd animate-header-example

To handle devices with notch both on iOS and Android operating systems, let’s install some libraries first. These libraries are going to add automatic padding on notch devices such that the main view of the app does not intersect with a safe area on notch-enabled devices. Run:

expo install react-native-safe-area-view react-native-safe-area-context

To use safe area views, wrap the root of the React Native app with SafeAreaProvider from the react-native-safe-area-context library. Open App.js and modify the it as shown below:

import React from "react";
import { Text, View } from "react-native";
import { SafeAreaProvider } from "react-native-safe-area-context";

export default function App() {
  return (
    <SafeAreaProvider>
      <View style={{ flex: 1, alignItems: "center" }}>
        <Text>Open up App.js to start working on your app!</Text>
      </View>
    </SafeAreaProvider>
  );
}

Next, wrap the contents of the App component with SafeAreaView from the react-native-safe-area-view library. It is going to have a style prop with a flex of value 1 and another prop called forceInset. It’s important we add this, especially for some Android devices which might not behave as expected. This prop is going to force the application to add an inset padding on the content view. Setting the value of top: always will always imply that padding is forced at the top of the view.

// ... other import statements
import SafeAreaView from "react-native-safe-area-view";

export default function App() {
  return (
    <SafeAreaProvider>
      <SafeAreaView style={{ flex: 1 }} forceInset={{ top: "always" }}>
        <View style={{ flex: 1, alignItems: "center" }}>
          <Text>Open up App.js to start working on your app!</Text>
        </View>
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

Here is what happens on an Android device when forceInset is not used on SafeAreaView:

ss1

And with the forceInset prop applied:

ss2

On iOS, the behavior is as expected:

ss3

The last step in this section is to create a new component file called AnimatedHeader.js inside the components/ directory. For now, it is going to return nothing.

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

const AnimatedHeader = () => {
  return null;
};

export default AnimatedHeader;

Make sure to import it in the App.js file:

// ... after other import statements
import AnimatedHeader from "./components/AnimatedHeader";

Creating an animated header component

The animation on the position of the scroll on a ScrollView component is going to have an Animated.Value of 0. To create an animation, Animated.Value is required. In the App.js file, import useRef from the React library. Then, define a variable called offset with a new Animated.Value. To use the Animated library from React Native, import it as well.

import React, { useRef } from "react";
import { Text, View, Animated } from "react-native";
// ...other import statements

export default function App() {
  const offset = useRef(new Animated.Value(0)).current;

  // ...
}

For this example, it is not required to use the useRef hook; however, if you are looking forward to modifying the animated value, it is recommended to use useRef. It provides a current property that is persisted throughout a component’s lifecycle.

The value of the offset can now be passed as a prop to the AnimatedHeader component.

export default function App() {
  const offset = useRef(new Animated.Value(0)).current;

  return (
    <SafeAreaProvider>
      <SafeAreaView style={{ flex: 1 }} forceInset={{ top: "always" }}>
        {/* Add the following AnimatedHeader */}
        <AnimatedHeader animatedValue={offset} />
        <View style={{ flex: 1, alignItems: "center" }}>
          <Text>Open up App.js to start working on your app!</Text>
        </View>
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

To access the safe area inset value inside the AnimatedHeader component, the library react-native-safe-area-context provides a hook called useSafeAreaInsets(). This hook returns a safe area insets object with the following values:

{
  top: number,
  right: number,
  bottom: number,
  left: number
}

The inset value of top is going to be manipulated when defining the animated header.

First, let’s import this hook in the AnimatedHeader.js file and then define a fixed HEADER_HEIGHT constant that is going to be the initial height of the Animated.View.

// ... other import statements
import { useSafeAreaInsets } from "react-native-safe-area-context";

const HEADER_HEIGHT = 200;

const AnimatedHeader = ({ animatedValue }) => {
  const insets = useSafeAreaInsets();

  return null;
};

To animate the height of the header view on the scroll, we are going to 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 is going to change the scale of the header to slide to the top on scroll along the y-axis. This effect is going to 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.

Begin by declaring a variable called headerHeight that is going to have the value of interpolation. The Animated.Value is the prop animatedValue coming from the parent component.

The inputRange is going to be 0 to the HEADER_HEIGHT plus the top inset. The outputRange is to be the HEADER_HEIGHT plus the top inset to the top inset plus 44.

const AnimatedHeader = ({ animatedValue }) => {
  const insets = useSafeAreaInsets();

  const headerHeight = animValue.interpolate({
    inputRange: [0, HEADER_HEIGHT + insets.top],
    outputRange: [HEADER_HEIGHT + insets.top, insets.top + 44],
    extrapolate: "clamp",
  });

  // ...
};

Now, let’s add an Animated.View to render from this component. It is going to use position: absolute to help cover the background behind the status bar as well as the same color as the whole header.

const AnimatedHeader = ({ animatedValue }) => {
  // ...
  return (
    <Animated.View
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        zIndex: 10,
        height: headerHeight,
        backgroundColor: "lightblue",
      }}
    />
  );
};

This section ends with the following output:

ss4

Manipulating the ScrollView

In the App.js file, a ScrollView component is going to be displayed beneath the header component and, in return, it is going to display a list of mocked data.

For this example, I’ve prepared a bare minimum list of book titles in a separate file called data.js.

const DATA = [
  {
    id: 1,
    title: "The Hunger Games",
  },
  {
    id: 2,
    title: "Harry Potter and the Order of the Phoenix",
  },
  {
    id: 3,
    title: "To Kill a Mockingbird",
  },
  {
    id: 4,
    title: "Pride and Prejudice",
  },
  {
    id: 5,
    title: "Twilight",
  },
  {
    id: 6,
    title: "The Book Thief",
  },
  {
    id: 7,
    title: "The Chronicles of Narnia",
  },
  {
    id: 8,
    title: "Animal Farm",
  },
  {
    id: 9,
    title: "Gone with the Wind",
  },
  {
    id: 10,
    title: "The Shadow of the Wind",
  },
  {
    id: 11,
    title: "The Fault in Our Stars",
  },
  {
    id: 12,
    title: "The Hitchhiker's Guide to the Galaxy",
  },
  {
    id: 13,
    title: "The Giving Tree",
  },
  {
    id: 14,
    title: "Wuthering Heights",
  },
  {
    id: 15,
    title: "The Da Vinci Code",
  },
];

export default DATA;

The next step is to import this file in App.js. Also, import the ScrollView component from React Native.

//...
import { ScrollView, Text, View, Animated } from "react-native";

import DATA from "./data";

Next, modify the contents of the App component. The important prop to note below in the ScrollView component is the onScroll prop. Mapping gestures like scrolling directly to an animated value can be done by using Animated.Event. This type of event function is passed as the value to the onScroll prop.

Animated.Event accepts an array of objects as the first argument which is going to be the contentOffset, which tells the current position of the scrolling view. It changes every time the user scrolls up or down. The value of contentOffset along the y-axis is going to be the same Animated.Value that is used to interpolate the height of the AnimatedHeader component.

It is recommended that you pass the second argument of useNativeDriver in Animated.Event .

export default function App() {
  const offset = useRef(new Animated.Value(0)).current;

  return (
    <SafeAreaProvider>
      <SafeAreaView style={{ flex: 1 }} forceInset={{ top: "always" }}>
        <AnimatedHeader animatedValue={offset} />
        <ScrollView
          style={{ flex: 1, backgroundColor: "white" }}
          contentContainerStyle={{
            alignItems: "center",
            paddingTop: 220,
            paddingHorizontal: 20,
          }}
          showsVerticalScrollIndicator={false}
          scrollEventThrottle={16}
          onScroll={Animated.event(
            [{ nativeEvent: { contentOffset: { y: offset } } }],
            { useNativeDriver: false }
          )}
        >
          {DATA.map(item => (
            <View
              key={item.id}
              style={{
                marginBottom: 20,
              }}
            >
              <Text style={{ color: "#101010", fontSize: 32 }}>
                {item.title}
              </Text>
            </View>
          ))}
        </ScrollView>
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

Here is the output after this step on an iOS device:

ss5

On Android:

ss6

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 that’s the part of the process.

Some of the important topics covered in this post are listed as links for further reading below:

Originally published at Jscrambler.

I'm a software developer and a technical writer. On this blog, I write about my learnings in software development and technical writing.

Currently, working as a documentation lead at Expo.