Native Shared Element Animations – react-native-shared-hero

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

Android Demo
iOS Demo

Features

  • Native shared element flights in Swift and Kotlin.
  • Router-agnostic matching with a combined namespace::id key.
  • A window-level overlay that can cross native-stack screens, modals, sheets, tabs, and React Native’s core Modal.
  • snapshot transitions for a cloned view that translates, scales, and crossfades.
  • morph transitions for bounds, corner radius, and background color interpolation.
  • Duration-based timing or a spring configuration.
  • Linear and curved arc motion paths.
  • cross, in, out, and through fade modes.
  • iOS return flights that follow the native-stack edge swipe and sheet swipe-down gestures.
  • enabled and returnFlightEnabled controls for individual views.
  • onTransitionStart and onTransitionEnd callbacks.
  • An optional useSharedHero hook for same-screen state changes.

Installation

Install the package with your preferred package manager:

npm install react-native-shared-hero
yarn add react-native-shared-hero

For a bare iOS project, install CocoaPods dependencies after adding the package:

cd ios
pod install

Build 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:ios

Basic 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:

PropTypeDefaultDescription
idstringRequiredStable identifier matched across views.
namespacestring"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.
durationnumber320Time-based flight duration in milliseconds.
spring{ damping?: number; stiffness?: number; mass?: number }NoneSpring 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.
enabledbooleantrueTurns participation off while the view stays mounted.
returnFlightEnabledbooleantrueControls the return flight when the view unregisters.
onTransitionStart(event: { id: string; namespace: string }) => voidNoneRuns on the source view when its outbound flight starts.
onTransitionEnd(event: { id: string; namespace: string }) => voidNoneRuns 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>

Add Comment