Demystifying React Native’s Animated API: Part 1

Simon Steer
Drop Engineering
Published in
10 min readSep 30, 2020

--

At Drop, we use React Native and TypeScript to develop our mobile app. Being able to write platform-agnostic code in a widely-adopted framework makes the switch from web development in React to mobile development in React Native relatively easy. As someone who loves the UX side of things, I found the biggest challenge when learning React Native was wrapping my head around the difference in mental models for animations.

By the time you are done reading this article, you should have a decent understanding of React Native’s Animated API’s mental model. This blog post is the first of a two-part series; in this post we will cover the basics of the Animated API by rebuilding an HTML/CSS button element and its hover states in React Native. In a follow-up blog post we’ll cover some more advanced concepts by building out a performant confetti animation step-by-step.

A brief overview of the differences in styling between React Native and React for web

On the web, you might animate a button using the CSS transition property like so:

.my-button {
padding: 8px 16px;
border-radius: 4px;
border: 2px solid black;
background: white;
outline: none;
color: black;
font-family: sans-serif;
font-weight: bold;
transition: color 0.2s, background 0.2s;
}
.my-button:hover {
background: black;
color: white;
}

Then, in your html:

<button class="my-button">
hover over me!
</button>

You may also use CSS keyframe animations to give it a bit more life:

@keyframes shake {
25% {
transform: rotate(3deg);
}
75% {
transform: rotate(-3deg);
}
}
.my-button:hover {
background: black;
color: white;
animation: shake 0.2s infinite linear;
}

In React Native, applying styles and animations to your elements doesn’t quite work the same way. Most notably:

  1. Styles in React Native are not applied via selectors, they are represented with objects. React Native provides a module, StyleSheet, which we can use to create a stylesheet via StyleSheet.create. Under the hood, StyleSheet.create registers the passed in object styles in an internally maintained list to refer back to when applying them to native views. As you may have guessed, no access to traditional CSS selectors also means no usage of :hover, :hover, :focus, etc. If you wish you apply your own custom states, you must animate them yourself.
  2. React Native styles do not cascade, and they do not support inheritance. Upper level styles will not “trickle down” to child element’s styles. For example, if you apply a font color to a parent view, its child views will not inherit the font color.
  3. React Native elements exclusively use Flexbox. Every element in RN follows the Flexbox model, and as such you cannot use CSS Grid, floats, or block display types.

This is just a brief list of what I consider to be the most important differences when it comes to styling, but you can take a look at the docs to learn more.

Animations in React Native

Here is how you might recreate the button from before without the shake animation or hover states in React Native:

import React from 'react'
import { TouchableWithoutFeedback, StyleSheet, Text } from 'react-native'
const styles = StyleSheet.create({
button: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 4,
borderWidth: 2,
borderColor: 'black',
backgroundColor: 'white',
flex: 0,
},
text: {
color: '#1e90ff',
fontFamily: 'sans-serif',
fontWeight: 'bold',
},
})
const Button = () => (
<TouchableWithoutFeedback
accessibilityRole="button"
accessibilityComponentType="button"
onPress={() => console.log('you pressed the button')}
style={styles.button}
>
<Text style={styles.text}>Press me!</Text>
</TouchableWithoutFeedback>
)

We create a stylesheet with the styles for both the button and the text. We do this because styles in React Native do not cascade/inherit. Since we can’t create stylesheet declarations for hover states in React Native, how do we go about animating the color change and shake animations? Instead of specifying fixed transitions for style properties when animating like we would on the web, we can interpolate a value over time to achieve natural, fluid motion, with greater control.

What the heck is interpolation?

Interpolation can be thought of as mapping one value to another. Let’s say we have a single value, that starts at 0, and increases to 1, over the duration of one second:

0 → 1

We can interpolate this value by creating a second value, which changes as our first value does, but with different upper and lower bounds. So we could, for example, interpolate our original value so as it moves from 0 to 1, our interpolated value moves from 2 to 7:

0 → 1

2 → 7

The Animated API lets us create values to animate and interpolate this way, and pass them as values to our element’s style objects. This allows us to think of the style properties we are animating as dynamic values, instead of fixed, time-based transitions.

Back to the Button

To put the concept of interpolation into practice here, let’s start by animating the button’s color. Since there is no cursor on mobile applications, the concept of a “hover” state doesn’t really exist, so we will make the button’s color change when the user has their finger pressed down.

We will start by creating an animated value and storing a reference to it in our React component. This is the value we will interpolate over time.

// import necessary modules
import { useRef } from 'react'
import { Animated } from 'react-native'
...// store a reference to an animated value in your component. We use useRef here otherwise we will create a new value on every render.const animatedValue = useRef(new Animated.Value(0)).current

Now that we have an Animated.Value to work with, let’s interpolate it! Animated.Values have an interpolate method which we can use to describe the mappings we want to apply. React Native does some magic under the hood which allows us to interpolate CSS color strings, as well as rgb/rgba values:

const animatedValue = useRef(new Animated.Value(0)).current/* inputRange represents the bounds of your value. Numbers provided to inputRange must monotonically increase. *//* outputRange length must equal inputRange length, represents what values the bounds map to */const backgroundColor = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: ['white', 'black'],
})

We now have our interpolation for the background color mapped out: When our animated value is at 0, our backgroundColor variable is white, and as our animated value approaches 1, our backgroundColor variable darkens to black.

Now we can use our interpolated value as the background color of the button; when we want the color of the button to animate to black, we increase our animated value to 1. We’ll also move our button’s styles to an Animated.View, since the Animated API requires that we use its special components if we want to animate an element’s style props in this way.

const Button = () => {
const animatedValue = useRef(new Animated.Value(0)).current
const backgroundColor = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: ['white', 'black'],
})
return (
<TouchableWithoutFeedback
accessibilityRole="button"
accessibilityComponentType="button"
onPress={() => console.log('you pressed the button')}
>
<Animated.View style={[styles.button, { backgroundColor }]}>
<Text style={styles.text}>Press me!</Text>
</Animated.View>
</TouchableWithoutFeedback>
)
}

The last step is to animate our value to 1 when the button is pressed down, and animate it back to 0 when the button is released. The Animated API provides some helpful methods specifically for animating Animated.Values. Most commonly, you’ll end up using Animated.timing and Animated.spring. For the purposes of this example, we will use the method with the easiest mental model, Animated.timing.

const handlePressIn = () =>
Animated.timing(animatedValue, {
toValue: 1,
duration: 300,
useNativeDriver: false, // <-- more on this in part 2 ;)
}).start()
const handlePressOut = () =>
Animated.timing(animatedValue, {
toValue: 0,
duration: 100,
useNativeDriver: false,
}).start()

Animated.timing takes two arguments; an Animated.Value instance and a configuration object which describes the timing of our animation. For more information on the available configuration options, you can check out the docs.

Putting It All Together

const Button = () => {
const animatedValue = useRef(new Animated.Value(0)).current

const backgroundColor = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: ['white', 'black'],
})
const handlePressIn = () =>
Animated.timing(animatedValue, {toValue: 1,
duration: 100,
useNativeDriver: false,
}).start()
const handlePressOut = () =>
Animated.timing(animatedValue, {
toValue: 0,
duration: 100,
useNativeDriver: false,
}).start()
return (
<TouchableWithoutFeedback
accessibilityRole="button"
accessibilityComponentType="button"
onPress={() => console.log('you pressed the button')}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
<Animated.View style={[styles.button, { backgroundColor }]}>
<Text style={styles.text}>Press me!</Text>
</Animated.View>
</TouchableWithoutFeedback>
)
}

Now when a user presses down on our custom button component, it will animate to black, and when the button is released, will animate back to white. To recap how we got here:

  1. We stored a reference to an Animated.Value instance in our component.
  2. We created an interpolated value such that when our Animated.Value is at 0, our interpolated value is the color white, and when our Animated.Value is at 1, our interpolated value is the color black.
  3. We made some functions using the Animated.timing method to animate our value from 0 to 1 and back again.
  4. We mapped these functions to some press events on our custom button.

The only part of the animation we are lacking is the shake that we made our browser-rendered button do. We have two options here:

  1. Create a new Animated.Value to map to the rotation of the button. This would probably be the easiest way to achieve what we want.
  2. Reuse our Animated.Value from before so everything runs on a single value. This is a more complicated solution, but results in fewer values to keep track of, and is more performant.

Since we already pursued option (1) when animating the color of our button, let’s use option (2) to make our button undergo the shaking animation when pressed down. Let’s start by creating another interpolated value for our button’s rotate transform, and applying it to our button’s styles:

const rotate = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '3deg'],
})
...return (
<TouchableWithoutFeedback
accessibilityRole="button"
accessibilityComponentType="button"
onPress={() => console.log('you pressed the button')}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
<Animated.View
style={[
styles.button,
{ backgroundColor, transform: [{ rotate }] }, // <-- here
]}
>
<Text style={styles.text}>Press me!</Text>
</Animated.View>
</TouchableWithoutFeedback>
)

We now have our button’s color shifting to black, and rotating 3 degrees when pressed down. But we need the button to not just rotate and sit at 3 degrees of rotation; we need it to shake shake, shake shake, shake, and oscillate between 3 and -3 degrees of rotation.

First, let’s store a reference to a boolean value in our component that will represent whether our button is pressed or not:

const isPressed = useRef(false)

We’re using useRef here, and not useState, for two reasons.

  1. Animated methods do not respect the order of execution of React hooks because they do not run on the React thread. This means that if we rely on the return value from useState to determine when to animate our button’s styles, we would not get the result we want because Animated methods won’t wait for our hooks to finish running.
  2. We could enforce usage of useState by wrapping our animation methods with useCallback hooks, for example, but we would be causing unnecessary renders, since the animations happen independent of React’s hook cycle anyway.

Next, let’s modify our methods to update this value when pressed and released.

const handlePressIn = () => {
isPressed.current = true
Animated.timing(animatedValue, {
toValue: 1,
duration: 100,
useNativeDriver: false,
}).start()
}
const handlePressOut = () => {
isPressed.current = false
Animated.timing(animatedValue, {
toValue: 0,
duration: 100,
useNativeDriver: false,
}).start()
}

We can then extend our rotation interpolation to support our animated value going from 0 → 1 → 2 instead of just from 0 → 1. When we animate our value from 0 to 1, the button will shift in color from white to black and rotate 3 degrees. When we animate our value to 2, our button will remain black, but will rotate to -3 degrees.

const rotate = animatedValue.interpolate({
inputRange: [0, 1, 2],
outputRange: ['0deg', '3deg', '-3deg'],
})

Now we can write a method that will animate our value from 1 → 2 and back again repeatedly to simulate the shake animation. We can use the Animated.sequence method to queue up multiple animations and have them run in order, then pass our shakeButton function as the callback to .start() so it re-executes on completion.

const shakeButton = () => {
// don’t run the animation while the button is not pressed
if (!isPressed.current) {
return
}
Animated.sequence([
Animated.timing(animatedValue, {
toValue: 2,
duration: 80,
useNativeDriver: false,
}),
Animated.timing(animatedValue, {
toValue: 1,
duration: 80,
useNativeDriver: false,
}),
]).start(shakeButton) // <-- reruns on completion
}

Finally, let’s have the shakeButton animation start once we have pressed down on the button and it has animated to black:

const handlePressIn = () => {
isPressed.current = true
Animated.timing(animatedValue, {
toValue: 1,
duration: 100,
useNativeDriver: false,
}).start(shakeButton) // <-- add this
}

And here is the full component code:

import React, { useRef } from 'react'
import {
Animated,
StyleSheet,
Text,
TouchableWithoutFeedback
} from 'react-native'
const styles = StyleSheet.create({
button: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 4,
borderWidth: 2,
borderColor: 'black',
backgroundColor: 'white',
flex: 0,
},
text: {
color: '#1e90ff',
fontFamily: 'sans-serif',
fontWeight: 'bold',
},
})
export const Button = () => {
const animatedValue = useRef(new Animated.Value(0)).current
const isPressed = useRef(false)

const backgroundColor = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: ['white', 'black'],
})

const rotate = animatedValue.interpolate({
inputRange: [0, 1, 2],
outputRange: ['0deg', '3deg', '-3deg'],
})
const shakeButton = () => {
// don’t run the animation while the button is not pressed
if (!isPressed.current) {
return
}
Animated.sequence([
Animated.timing(animatedValue, {
toValue: 2,
duration: 80,
useNativeDriver: false,
}),
Animated.timing(animatedValue, {
toValue: 1,
duration: 80,
useNativeDriver: false,
}),
]).start(shakeButton)
}
const handlePressIn = () => {
isPressed.current = true
Animated.timing(animatedValue, {
toValue: 1,
duration: 100,
useNativeDriver: false,
}).start(shakeButton)
}
const handlePressOut = () => {
isPressed.current = false
Animated.timing(animatedValue, {
toValue: 0,
duration: 100,
useNativeDriver: false,
}).start()
}
return (
<TouchableWithoutFeedback
accessibilityRole="button"
accessibilityComponentType="button"
onPress={() => console.log('you pressed the button')}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
<Animated.View
style={[
styles.button,
{ backgroundColor, transform: [{ rotate }] },
]}
>
<Text style={styles.text}>Press me!</Text>
</Animated.View>
</TouchableWithoutFeedback>
)
}

Here is a comparison of the button we animated for the web, versus the button we animated in React Native:

Button animated on the web using HTML/CSS
Button animated in React Native using the Animated API

That’s all for part one of this series! Hopefully you now have a decent understanding of the mental model of the Animated API, and have enough knowledge to begin making your own animations in React Native. Stay tuned for part two, where we take the concepts presented here and build a complex, performant confetti animation together!

--

--