Building smooth Shared Element Transitions in React Native

2023-03-09
0 views

Last week, the Software Mansion team released react-native-reanimated 3.0.0, which brings a lot of new exciting features and improved stability to the animation library.

Some parts of the Reanimated core codebase have been rewritten from the ground up, such as isAnimated, the Shared Value implementation, and other performance critical parts.

Additionally, they introduced a new feature - Shared Element Transitions - and in this blogpost I'm going to go over all you need to know to use Shared Element Transitions in your app! 🚀

So.. What are Shared Element Transitions? Shared Element Transitions (or "Hero" animations) are screen navigation animations, that animate an element between two screens. Lots of apps are using these animations nowadays, for example the Instagram app implemented Shared Element Transitions for their Post View:

Shared Element Transitions Demo

Without the right tools, implementing such an animation is insanely difficult, as you would need to take spacings, safe areas, screen coordinates, view width/height, border radius, opacity, image resizeMode, and many other things into account to create a smooth experience.

With Reanimated V3, things got a lot easier, so let's dive straight in!


Setting up your project

Create a new React Native (or Expo) app:

npx react-native init SharedElementExample

Then, install Reanimated:

yarn add react-native-reanimated

Make sure you installed version 3.0.2 or higher!

And lastly, install the navigation library:

yarn add @react-navigation/native         # navigation base library
yarn add @react-navigation/native-stack   # the stack we use
yarn add react-native-screens             # required for native stack
yarn add react-native-safe-area-context   # required for safe-area

Shared Element Transitions only work with the native stack navigator (react-native-screens). The native stack navigator is faster anyways, so I recommend using that in every situation.

Let's start coding!

First, create the root component and entry point for our app (App.tsx):

./App.tsx
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { Home } from './screens/Home'
import { CarDetails } from './screens/CarDetails'
 
const Stack = createNativeStackNavigator()
 
export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={Home} />
        <Stack.Screen name="CarDetails" component={CarDetails} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

This is the main navigator component that connects the two screens (Home and CarDetails) and mounts them in a Native Stack. Navigation between those screens is handled using react-navigation.

Next, implement the two screens. Let's start with Home:

./screens/Home.tsx
function Home({ navigation }) {
  return (
    <View style={{ flex: 1 }}>
      <Animated.View
        style={{ width: 150, height: 150, backgroundColor: 'green' }}
        sharedTransitionTag="sharedTag"
      />
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('CarDetails')}
      />
    </View>
  );
}

We created an Animated.View (from react-native-reanimated), and gave it a sharedTransitionTag. This is the tag that Reanimated uses to find out which elements to connect in a Shared Element Transition. Next, let's implement CarDetails and give the Animated.View the same sharedTransitionTag:

./screens/CarDetails.tsx
function CarDetails({ navigation }) {
  return (
    <View style={{ flex: 1, marginTop: 50 }}>
      <Animated.View
        style={{ width: 100, height: 100, backgroundColor: 'green' }}
        sharedTransitionTag="sharedTag"
      />
      <Button
        title="Go back"
        onPress={() => navigation.navigate('Home')}
      />
    </View>
  );
}

Okay - let's run it! Here's how it looks:

Shared Element Transitions Simple Demo

Note: If you can't see that transition happening, make sure to clean your cache (node_modules, yarn.lock, ios/Pods) and try rebuilding your app. Also make sure you're on Reanimated V3, and that both of your views have the same sharedTransitionTag.

Making it butter-smooth!

The animation works, but it feels a bit rough.

Let's customize the animation a bit!

You can use the sharedTransitionStyle prop on the Animated.View to pass a custom Reanimated animation Worklet to the Shared Element Transition. This is similar to a useAnimatedStyle, but with target values that you need to animate to. Let's create a helper file:

./utils/SharedElementTransition.ts
import Animated, {
  SharedTransition,
  withSpring,
  WithSpringConfig,
} from 'react-native-reanimated'
 
const SPRING_CONFIG = {
  mass: 1,
  stiffness: 100,
  damping: 200,
}
 
export const sharedElementTransition = SharedTransition.custom((values) => {
  'worklet'
  return {
    height: withSpring(values.targetHeight, SPRING_CONFIG),
    width: withSpring(values.targetWidth, SPRING_CONFIG),
    originX: withSpring(values.targetGlobalOriginX, SPRING_CONFIG),
    originY: withSpring(values.targetGlobalOriginY, SPRING_CONFIG),
  }
})

And then, let's use that in both of our Views:

// ...
  <Animated.View
        style={{ width: 100, height: 100, backgroundColor: 'green' }}
        sharedTransitionTag="sharedTag"
        sharedTransitionStyle={sharedElementTransition}
  />
// ...

Much better!

Shared Element Transitions Simple Demo


Conclusion

Reanimated Shared Element Transitions provide a great foundation for building smooth animations across screens, but as with all animations, use them sparingly as you don't want to overwhelm the user with moving things.

In a production app, you might use Shared Element Transitions for main Images, main Text, important Views and more. Animations are a way to underline the importance of some specific content in your app, for example in this app I'm working on, the user can view the details of a car.

Shared Element Transition Navigation

The common view in both screens is the car's image, so I want to underline the importance of that image by animating it along with the screen transition. Here's how that looks:

Shared Element Transitions Demo

As of Reanimated 3.0.2, Shared Element Transitions are not yet production ready. The Software Mansion team is taking feedback for the new feature, so make sure to try it out!

Shoutout to the @swmansion team for building this feature, I'm really excited for SETs!

wix/react-native-navigation

If you're using Wix's react-native-navigation library instead of react-navigation, you can already use SETs in your app!

I've worked on this quite a while ago, but here's a tweet showcasing react-native-navigation's Shared Element Transitions: https://twitter.com/mrousavy/status/1319232047764918272

Sources

  1. https://reactnavigation.org
  2. https://docs.swmansion.com
Liked this blog post?Share this on Twitter💖  Sponsor me on GitHub