Description:
react-native-shared-hero is a React Native component library that creates native shared element transitions between matching views on iOS and Android.
It connects images and cards across native-stack screens, modals, sheets, tabs, and in-place state changes with snapshot and morph modes.
See It In Action
Features
- Native shared element flights in Swift and Kotlin.
- Router-agnostic matching with a combined
namespace::idkey. - A window-level overlay that can cross native-stack screens, modals, sheets, tabs, and React Native’s core
Modal. snapshottransitions for a cloned view that translates, scales, and crossfades.morphtransitions for bounds, corner radius, and background color interpolation.- Duration-based timing or a spring configuration.
- Linear and curved
arcmotion paths. cross,in,out, andthroughfade modes.- iOS return flights that follow the native-stack edge swipe and sheet swipe-down gestures.
enabledandreturnFlightEnabledcontrols for individual views.onTransitionStartandonTransitionEndcallbacks.- An optional
useSharedHerohook for same-screen state changes.
Installation
Install the package with your preferred package manager:
npm install react-native-shared-heroyarn add react-native-shared-heroFor a bare iOS project, install CocoaPods dependencies after adding the package:
cd ios
pod installBuild the native application after installation. A JavaScript reload inside an existing binary does not add the new native module.
Expo development build
Expo development builds are the supported Expo target. Expo Go cannot load this native module:
npx expo install react-native-shared-hero
npx expo prebuild
npx expo run:iosBasic shared image transition
Render the same id and namespace around the source and destination content. The source view unmounts and the destination view mounts within roughly one native frame, so the native registry can pair them.
import { Image, Pressable } from 'react-native';
import { SharedHero } from 'react-native-shared-hero';
export function PhotoTile({ photo, onPress }) {
return (
<Pressable onPress={onPress}>
<SharedHero
id={'photo-' + photo.id}
namespace="gallery"
style={styles.thumbnail}
>
<Image source={{ uri: photo.uri }} style={styles.fill} />
</SharedHero>
</Pressable>
);
}
export function PhotoDetail({ photo }) {
return (
<SharedHero
id={'photo-' + photo.id}
namespace="gallery"
style={styles.hero}
>
<Image source={{ uri: photo.uri }} style={styles.fill} />
</SharedHero>
);
}Choose a transition mode
Use snapshot for a straightforward image or content flight:
<SharedHero
id={'photo-' + photo.id}
namespace="gallery"
mode="snapshot"
duration={360}
fadeMode="cross"
motionPath="arc"
style={styles.hero}
>
<Image source={{ uri: photo.uri }} style={styles.fill} />
</SharedHero>Use morph for a card that changes size and shape between states:
<SharedHero
id={'card-' + photo.id}
namespace="cards"
mode="morph"
spring={{ damping: 16, stiffness: 200, mass: 1 }}
style={[styles.card, { backgroundColor: photo.color }]}
>
<Image source={{ uri: photo.uri }} style={styles.fill} />
</SharedHero>Same-screen state transitions with useSharedHero
Navigation-driven transitions need only two matching SharedHero components. For an in-place change, useSharedHero supplies a small state helper:
import { Pressable } from 'react-native';
import { SharedHero, useSharedHero } from 'react-native-shared-hero';
export function ExpandableCard({ photo }) {
const { active, toggle } = useSharedHero();
return (
<Pressable onPress={toggle}>
{active ? (
<SharedHero id="photo-card" namespace="in-place" mode="morph" style={styles.largeCard}>
<PhotoContent photo={photo} />
</SharedHero>
) : (
<SharedHero id="photo-card" namespace="in-place" mode="snapshot" style={styles.smallCard}>
<PhotoContent photo={photo} />
</SharedHero>
)}
</Pressable>
);
}Available Component Props
SharedHero is also exported as SharedHeroView. It accepts standard React Native ViewProps, including style and children, plus these props:
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | Required | Stable identifier matched across views. |
namespace | string | "default" | Isolates registries. The matching key is namespace::id. |
mode | "snapshot" | "morph" | "shuttle" | "zoom" | "auto" | "snapshot" | Selects the transition style. shuttle maps to snapshot; zoom and auto map to morph in v1. |
duration | number | 320 | Time-based flight duration in milliseconds. |
spring | { damping?: number; stiffness?: number; mass?: number } | None | Spring configuration. It takes precedence over duration. |
fadeMode | "cross" | "in" | "out" | "through" | "cross" | Controls source and destination fades. |
easing | "linear" | "easeIn" | "easeOut" | "easeInOut" | "standard" | "emphasized" | "standard" | Easing preset for duration-based flights. |
motionPath | "linear" | "arc" | "linear" | Center-point path for the flying element. |
enabled | boolean | true | Turns participation off while the view stays mounted. |
returnFlightEnabled | boolean | true | Controls the return flight when the view unregisters. |
onTransitionStart | (event: { id: string; namespace: string }) => void | None | Runs on the source view when its outbound flight starts. |
onTransitionEnd | (event: { id: string; namespace: string }) => void | None | Runs on the destination view when its inbound flight ends. |
Transition callbacks
The callbacks receive the matched hero identity. They are useful for analytics, temporary UI state, or coordinating content that should appear after the flight:
<SharedHero
id="profile-avatar"
namespace="profile"
onTransitionStart={({ id, namespace }) => {
console.log('Hero started', id, namespace);
}}
onTransitionEnd={({ id, namespace }) => {
console.log('Hero arrived', id, namespace);
}}
style={styles.avatar}
>
<Image source={avatarSource} style={styles.fill} />
</SharedHero>