Animating elements in React Native isn’t as straight forward as you may expect. If you’re used to animating elements with CSS, you may be used to using transition and transform, like such:

.element {
  transition: transform 1s ease;
}
.element:hover {
  transform: translateX(20px);
}

Unfortunately, React Native can’t animate vanilla CSS. Thankfully React Native comes baked with the Animated library to help you build animations.

Simple Toggle

Let‘s start by creating a simple toggle that has on-off states and changes state when you tap on it.

import React from "react"
import { TouchableOpacity, View } from "react-native"

export default class App extends React.Component {
  state = {
    isOn: false,
  }

  toggleHandle() {
    this.setState({ isOn: !this.state.isOn })
  }

  render() {
    return (
      <View>
        <TouchableOpacity
          activeOpacity={0.5}
          style={{
            width: 64,
            height: 32,
            borderRadius: 32,
            padding: 4,
            backgroundColor: this.state.isOn
              ? "limegreen"
              : "gray",
          }}
          onPress={() => this.toggleHandle()}
        >
          <View style={{
            width: 24,
            height: 24,
            borderRadius: 32,
            transform: [{
              translateX: this.state.isOn ? 32 : 0,
            }]
          }} />
        </TouchableOpacity>
      </View>
    )
  }
}

<TouchableOpacity> is the toggle container while the inner <View> is the knob.

State isOn determines whether the toggle is on and off; and if it’s on, the knob is being translated 32px along the X axis.

To animate, typically we’d transition the transform and call it a day:

transition: transform 0.2s ease;

But this is React Native…

Animated

The Animated library is the recommend way to animate in React Native. It comes with predefined methods you can use to animate, but for this tutorial, we’ll only cover Animated.timing. Be sure to read the Animated API documentation to learn more.

timing() animates a values along a timed easing curve. It accepts 2 parameters: timing(value, config). The first is the animated value and the second is a config object.

To use Animated, it needs to be imported:

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

We’ll use state animatedValue to set the initial state and manage the animated value of the toggle knob as the state changes.

state = {
  isOn: false,
  animatedValue: new Animated.Value(0),
}

We can then set the knob’s translateX as the value of state animatedValue:

<Animated.View style={{
  width: 24,
  height: 24,
  borderRadius: 32,
  transform: [{
    translateX: this.state.animatedValue,
  }]
}} />

We need to modify toggleHandle() to also animate the toggle based on the updated state. We’ll fire Animated.timing() in setState’s optional callback:

toggleHandle() {
  this.setState(
    { isOn: !this.state.isOn },
    () => {
      Animated.timing(
        this.state.animatedValue,
        { toValue: this.state.isOn ? 32 : 0 }
      ).start()
    }
  )
}

We can also customize the duration, delay, and easing of the animation:

import {
  Animated,
  Easing,
  TouchableOpacity,
  View,
} from "react-native"

...

Animated.timing(
  this.state.animatedValue,
  {
    toValue: this.state.isOn ? 32 : 0,
    duration: 250,         // in milliseconds, default is 500
    easing: Easing.bounce, // Easing function, default is Easing.inOut(Easing.ease)
    delay: 0,              // in milliseconds, default is 0
  }
).start()

The Easing module comes with a lot of predefined animations along with advanced functions for more complex ones:

easing: Easing.linear,
easing: Easing.bounce,
easing: Easing.elastic(0.7),
easing: Easing.bezier(0.75, -0.25, 0.25, 1.25),

Be sure to read more about it from the Easing documentation.

Here’s what the animated toggle looks like after putting it all together:

Stateful Toggle Component (Extra Credit)

The above solution works and is fine as an introduction to Animated, but it isn’t practical if there are more than one toggles to manage.

Instead, a reusable and composable solution like this would be ideal:

render() {
  return (
    <View>
      <Toggle
        isOn={this.state.alertsIsOn}
        onToggle={state => this.alertsToggleHandle(state)}
      />
      <Toggle
        isOn={this.state.newsletterIsOn}
        onToggle={state => this.newsletterToggleHandle(state)}
      />
      <Toggle
        isOn={this.state.trackingIsOn}
        onToggle={state => this.trackingToggleHandle(state)}
      />
    </View>
  )
}

Let’s begin by extracting a Toggle component.

// Toggle.js
import React from "react"
import PropTypes from "prop-types"
import {
  Animated,
  Easing,
  TouchableOpacity,
} from "react-native"

const knobOffset = 32

export class Toggle extends React.Component {
  static propTypes = {
    isOn: PropTypes.bool,
    onToggle: PropTypes.func.isRequired,
  }

  static defaultProps = {
    isOn: false,
  }

  state = {
    isOn: this.props.isOn,
    animatedValue: new Animated.Value(this.props.isOn ? knobOffset : 0),
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevProps.isOn !== this.props.isOn) {
      this.setState(
        { isOn: this.props.isOn },
        () => {
          Animated.timing(
            this.state.animatedValue,
            {
              toValue: this.state.isOn ? knobOffset : 0,
              easing: Easing.elastic(0.7),
              duration: 100,
            }
          ).start()
        }
      )
    }
  }

  handlePress() {
    this.setState(
      { isOn: !this.state.isOn },
      () => this.props.onToggle(this.state.isOn)
    )
  }

  render() {
    return (
      <TouchableOpacity
        activeOpacity={0.5}
        style={{
          backgroundColor: this.state.isOn ? "limegreen" : "gray",
          width: 64,
          height: 32,
          borderRadius: 32,
          padding: 4,
        }}
        onPress={() => this.handlePress()}
      >
        <Animated.View style={{
          width: 24,
          height: 24,
          borderRadius: 32,
          transform: [{
            translateX: this.state.animatedValue,
          }]
        }} />
      </TouchableOpacity>
    )
  }
}

Of the changes, the main thing to note is that we’ve moved Animated.timing() to the componentDidUpdate() lifecycle method.

This allows us to compose Toggle and more easily manage the state of each independently:

// App.js
import React from "react"
import { View } from "react-native"
import { Toggle } from "./Toggle"

export default class App extends React.Component {
  state = {
    alertsIsOn: true,
    newsletterIsOn: false,
    trackingIsOn: true,
  }

  alertsToggleHandle(state) {
    this.setState({ alertsIsOn: state })
  }
  newsletterToggleHandle(state) {
    this.setState({ newsletterIsOn: state })
  }
  trackingToggleHandle(state) {
    this.setState({ trackingIsOn: state })
  }

  render() {
    return (
      <View>
        <Toggle
          isOn={this.state.alertsIsOn}
          onToggle={state => this.alertsToggleHandle(state)}
        />
        <Toggle
          isOn={this.state.newsletterIsOn}
          onToggle={state => this.newsletterToggleHandle(state)}
        />
        <Toggle
          isOn={this.state.trackingIsOn}
          onToggle={state => this.trackingToggleHandle(state)}
        />
      </View>
    )
  }
}

I encourage you to dive into the Animated API! It’s pretty powerful and can help you build some pretty complex animations in your app.