How We Animate Header Titles in React Native

When creating our parallax headers in React Native, we encountered what seemed to be an impossible task: using the Animated.interpolate() function to transition our styles between two Flexbox properties.

Our intention to make our text transition from flex-start to center was unfortunately not as straightforward to implement as we had hoped. Looking at the documentation for Animated, we came to understand that while interpolate accepts strings for values, it is limited to colors and values with units.

We found ourselves in a place where we needed to get creative with the Animated API, in order to replicate our Flexbox transition another way.


Creating our animation

Above is a gif of what we will be building in this article. If you’d like to jump right to a working example, head over to https://snack.expo.io/SkK4P4UC-Otherwise, let’s dive in!

Initial setup

Here’s what you’ll need to get started:

  1. A component with a custom parallax header (for help setting one of these up check out any of these articles)
  2. A ScrollView, FlatList, etc in your component (we will utilize the onScroll event to trigger changes to our animation)
  3. Enough content in your scrolling list to test our animation

Step 1: Setting up our animated header

There are a variety of ways to set up your header, based on your use case. Here is our setup for this example; below the code you’ll find an explanation of the important parts (highlighted in bold) that will help us animate our text.

screenWidth = Dimensions.get('window').width;
<Animated.View
style={
flexDirection: 'row',
justifyContent: 'center',
paddingHorizontal: screenWidth * 0.05,
width: screenWidth,
alignItems: 'flex-end',
height: this.state.scrollOffset.interpolate({
inputRange: [0, 200],
outputRange: [120, 64],
extrapolate: 'clamp',
}),
}
>

The important parts (and why):

  1. flexDirection: 'row' — allows us to animate our text horizontally
  2. justifyContent: 'center' — allows us to control our text alignment
  3. paddingHorizontal — this gives us the exact width of the area where our title is rendered
  4. width: screenWidth — to ensure this truly spans the entire screen, we want to explicitly define this value (rather than setting it to '100%')

Step 2: Left-aligning our text whilst centering our content

You may have noted that our header styles containjustifyContent: 'center' — that will help us get things centered at the end of our animation.

In order to start our text in a left-aligned state, however, we must be a little clever. React Native provides a handy onLayout callback which informs us of our view’s exact size on the screen after it is rendered. Here is how we are utilizing onLayout for our <Animated.Text> component rendering our text:

onLayout={e => {
const titleWidth = e.nativeEvent.layout.width;
this.setState({ titleWidth });
}}

By storing the width of our header title text when it renders, we can push it left aligned by adding an invisible view directly to the right of it in our header.

Our header has width equivalent to the screen width, with 5% padding on both the left and right. Therefore, the space where our title is rendered has a width 90% of the screen width. With some simple math, we calculate the width of our invisible view as screenWidth * 0.9 — this.state.titleWidth.

Without any animations, we now have this:

<Animated.Text
onLayout={e => {
const titleWidth = e.nativeEvent.layout.width;
this.setState({ titleWidth });
}}
style={{
fontWeight: 'bold',
fontSize: 26,
}}
>
Header Title Here
</Animated.Text>
<Animated.View
style={{
width: screenWidth * 0.9 - this.state.titleWidth,
}}
/>

When we render this view next to our text, our text is left-aligned! Voila!

Left-align in a centered container! (our invisible view has some color here to show what’s going on)

Step 3: Animating the text alignment

Next, we simply need to animate the width of our invisible view from its initial width down to zero at the conclusion of our animation, and our text will be centered. You can add any other animations you like to your text, header, etc. Here’s our implementation:

<Animated.Text
onLayout={e => {
if (this.offset === 0 && this.state.titleWidth === 0) {
const titleWidth = e.nativeEvent.layout.width;
this.setState({ titleWidth });
}
}}
style={{
fontWeight: 'bold',
fontSize: scrollOffset.interpolate({
inputRange: [0, 200],
outputRange: [26, 20],
extrapolate: 'clamp',
}),
}}>
Header Title Here
</Animated.Text>
<Animated.View
style={{
width: scrollOffset.interpolate({
inputRange: [0, 200],
outputRange: [screenWidth * 0.9 - this.state.titleWidth, 0],
extrapolate: 'clamp',
}),
}}
/>

The Final Product

Now we just need to wire up our onScroll listener in our ScrollView and watch our animation in all its beauty! Check out the full working example here: https://snack.expo.io/SkK4P4UC-

A couple notes:

1 . We added some safeguards in the onLayout function of our <Animated.Text> component. We really only need to set the value of titleWidth once, upon initialization. The width of our rendered text (in its initial state) should not change, so we added an if-statement wrapping our setState function which confirmstitleWidth does not yet have a value and ensures the scroll offset is equal to zero.

2. An unhandled case in this example is where titleWidth > screenWidth * 0.9. I didn’t include it here for simplicity, but in our app we ellipse the text to fill the width, and keep the width of our invisible set at zero for the entire animation.


Thanks for reading! Please feel free to leave any questions, comments, suggestions, or concerns below.