id | title | sidebar_label |
---|---|---|
shared-element-transitions |
Animating elements between screens |
Shared element transitions |
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
This guide covers how to animate elements between screens. This feature is known as a Shared Element Transition and it's implemented in the @react-navigation/native-stack
with React Native Reanimated.
:::warning
As of writing this guide, Shared Element Transitions are considered an experimental feature not recommended for production use.
:::
Before continuing this guide make sure your app meets these criteria:
- You are using
@react-navigation/native-stack
. The Shared Element Transitions feature isn't supported in JS-based@react-navigation/stack
. - You have
react-native-reanimated
v3.0.0 or higher installed and configured. - You are using the old React Native architecture. The current implementation does not support the new architecture (Fabric).
To create a shared transition:
- Use
Animated
components imported fromreact-native-reanimated
. - Assign the same
sharedTransitionTag
to elements on different screens. - Navigate between screens. The transition will start automatically.
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import {
useNavigation,
createStaticNavigation,
} from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Button } from '@react-navigation/elements';
import Animated from 'react-native-reanimated';
function HomeScreen() {
const navigation = useNavigation();
return (
<View style={styles.container}>
<Button onPress={() => navigation.navigate('Details')}>
Go to Details
</Button>
<Animated.Image
source={{ uri: 'https://picsum.photos/id/39/200' }}
style={{ width: 300, height: 300 }}
// highlight-next-line
sharedTransitionTag="tag"
/>
</View>
);
}
function DetailsScreen() {
const navigation = useNavigation();
return (
<View style={styles.container}>
<Button onPress={() => navigation.goBack()}>Go back</Button>
<Animated.Image
source={{ uri: 'https://picsum.photos/id/39/200' }}
style={{ width: 100, height: 100 }}
// highlight-next-line
sharedTransitionTag="tag"
/>
</View>
);
}
// highlight-start
const RootStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Details: DetailsScreen,
},
});
// highlight-end
const Navigation = createStaticNavigation(RootStack);
export default function App() {
return <Navigation />;
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
},
});
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { NavigationContainer, useNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Button } from '@react-navigation/elements';
import Animated from 'react-native-reanimated';
// highlight-next-line
const Stack = createNativeStackNavigator();
function HomeScreen() {
const navigation = useNavigation();
return (
<View style={styles.container}>
<Button onPress={() => navigation.navigate('Details')}>
Go to Details
</Button>
<Animated.Image
source={{ uri: 'https://picsum.photos/id/39/200' }}
style={{ width: 300, height: 300 }}
// highlight-next-line
sharedTransitionTag="tag"
/>
</View>
);
}
function DetailsScreen() {
const navigation = useNavigation();
return (
<View style={styles.container}>
<Button onPress={() => navigation.goBack()}>Go back</Button>
<Animated.Image
source={{ uri: 'https://picsum.photos/id/39/200' }}
style={{ width: 100, height: 100 }}
// highlight-next-line
sharedTransitionTag="tag"
/>
</View>
);
}
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
},
});
sharedTransitionTag
is a string that has to be unique in the context of a single screen, but has to match elements between screens. This prop allows Reanimated to identify and animate the elements, similarly to the key
property, which tells React which element in the list is which.
By default, the transition animates the width
, height
, originX
, originY
and transform
properties using withTiming
with a 500 ms duration. You can easily customize width
, height
, originX
, and originY
props. Customizing transform
is also possible but it's far beyond the scope of this guide.
:::warning
Custom SharedTransition API is not finalized and might change in a future release.
:::
To customize the transition you need to pass all the properties besides transform
.
import { SharedTransition } from 'react-native-reanimated';
const customTransition = SharedTransition.custom((values) => {
'worklet';
return {
height: withSpring(values.targetHeight),
width: withSpring(values.targetWidth),
originX: withSpring(values.targetOriginX),
originY: withSpring(values.targetOriginY),
};
});
function HomeScreen() {
return (
<Animated.Image
style={{ width: 300, height: 300 }}
sharedTransitionTag="tag"
// highlight-next-line
sharedTransitionStyle={customTransition} // add this to both elements on both screens
/>
);
}
You can find a full Shared Element Transitions reference in the React Native Reanimated documentation.
Alternatively, you can use react-native-shared-element
library with a React Navigation binding which implements Shared Element Transitions in a JS-based @react-navigation/stack
navigator. This solution, however, isn't actively maintained.
The react-native-navigation
also comes with support for Shared Element Transitions. You can read more about it here.