npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-shared-transition

v0.2.1

Published

Automatic shared element transitions for React Native — New Architecture, Nitro Modules, Reanimated

Readme


Why?

Shared element transitions in React Native have been stuck between two options:

  • react-native-shared-element — the classic solution, but unmaintained, built for the old architecture, and its react-navigation-shared-element binding never made it past react-navigation v5.
  • Reanimated's sharedTransitionTag — promising, but still marked experimental and limited in how transitions can be customized.

react-native-shared-transition is a small, focused take on the problem for modern React Native (Fabric, react-navigation v7, Reanimated 3/4):

  • 🧭 Automatic — mount two <SharedElement id="..."> with the same id on two screens; the transition (and its reverse on back navigation) just happens.
  • 🏎️ Nitro-powered measurement — element frames are measured natively (JSI, no bridge) in window coordinates; originals are hidden natively while the overlay flies.
  • 🎬 UI-thread animation — the overlay is driven by Reanimated springs/timing on the UI thread.
  • 🔄 Interruption-safe — navigating back mid-flight retargets the running spring instead of jumping; rapid taps don't leak overlays or leave elements hidden.
  • 🎨 Morphs what matters — position, size, borderRadius (rounded card → circle), image resizeMode, plus optional cross-fade for content that differs (e.g. text at different sizes).

Requirements

| | | | --- | --- | | React Native | 0.76+ (New Architecture) | | react-native-nitro-modules | ^0.32 | | react-native-reanimated | ≥ 3.6 | | iOS | 13+ | | Android | API 24+ |

Installation

yarn add react-native-shared-transition react-native-nitro-modules react-native-reanimated
cd ios && pod install

react-native-nitro-modules and react-native-reanimated are peer dependencies. Reanimated also needs its babel plugin (react-native-worklets/plugin on Reanimated 4).

Quick start

1. Wrap your app with the host (it renders the transition overlays):

import { SharedTransitionHost } from 'react-native-shared-transition';

export default function App() {
  return (
    <SharedTransitionHost>
      <NavigationContainer>
        <RootNavigator />
      </NavigationContainer>
    </SharedTransitionHost>
  );
}

2. Use a fade (or none) animation on the participating screens — sliding screen animations would move the measured target mid-flight:

<Stack.Navigator screenOptions={{ animation: 'fade', animationDuration: 350 }}>
  <Stack.Screen name="Home" component={HomeScreen} />
  <Stack.Screen name="Detail" component={DetailScreen} />
</Stack.Navigator>

3. Mark the shared elements — same id on both screens:

// HomeScreen — grid card
<SharedElement id={`hero.${hero.id}.photo`}>
  <Image source={hero.photo} style={styles.thumb} />   {/* 110×110, borderRadius 20 */}
</SharedElement>

// DetailScreen — hero header
<SharedElement id={`hero.${hero.id}.photo`}>
  <Image source={hero.photo} style={styles.hero} />    {/* 220×220, borderRadius 110 */}
</SharedElement>

That's it. Pushing Detail morphs the card into the hero (including the radius); going back morphs it home again.

Configuration

Pass defaults on the host and/or override per element:

<SharedTransitionHost config={{ animation: 'spring', spring: { damping: 24, stiffness: 220 } }}>

<SharedElement id="hero.name" config={{ contentScale: 'transform', crossFade: true }}>
  <Text style={styles.title}>{hero.name}</Text>
</SharedElement>

<SharedElement id="hero.photo" config={{ animation: 'timing', duration: 450, easing: 'ease-in-out' }}>

| Option | Type | Default | Description | | --- | --- | --- | --- | | animation | 'spring' \| 'timing' | 'spring' | Animation driver | | spring | { damping?, stiffness?, mass?, overshootClamping? } | { 24, 220, 1, false } | Spring parameters | | duration | number | 320 | Duration in ms (timing) | | easing | 'linear' \| 'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' | 'ease-in-out' | Easing (timing) | | crossFade | boolean | false | Cross-fade source → target content (for differing content, e.g. text) | | morphBorderRadius | boolean | true | Animate borderRadius between the two styles | | contentScale | 'resize' \| 'transform' | 'resize' | 'resize' re-lays-out content each frame (images); 'transform' scales a fixed layout (text) |

Full reference (hooks, native primitives): docs/api.md.

How it works

  1. <SharedElement> wraps its child in a View with a unique nativeID and registers it (with a clone template of the child) in a JS registry.
  2. When a second element with the same id mounts (screen push), the coordinator waits for its first layout, then measures both endpoints through the Nitro module — natively, in the root view's coordinate space. On iOS the measurement is transform-aware (the rect is mapped through the full transform chain); on Android it currently reports the layout box, so an element that is mid-scale measures its unscaled size.
  3. A Reanimated-driven overlay is mounted at the source frame; only once it exists are the two originals hidden natively (alpha, layout-free) — so there is never an empty frame.
  4. The overlay morphs x/y/width/height/borderRadius to the target frame with the configured spring/timing. Content is a re-rendered clone of the child (same image source ⇒ zero snapshot I/O, no decode flicker), optionally cross-faded.
  5. On completion the target is revealed first and the overlay removed one frame later — a seamless hand-off. On back navigation the coordinator replays the morph in reverse from the element's last measured frame; if that happens mid-flight, the running spring is retargeted instead of restarted.

The Nitro module (measureNode, captureSnapshot, setNodeHidden, cleanup) is a plain Swift/Kotlin HybridObject — no TurboModule codegen, no bridge serialization. captureSnapshot (native PNG capture) is exposed for advanced/custom pipelines.

Example app

The example app is a photo gallery — a hero card plus a masonry grid — where every frame flies into a full-bleed detail screen. Each card shares two elements at once: the photograph and its title (the latter with contentScale: 'transform' + crossFade, since it changes size between screens).

A segmented control at the top switches the app-wide config passed to <SharedTransitionHost config> between a default spring, a fixed timing curve, and an under-damped bouncy spring, so you can feel the difference on the same transition. It is dark-first and follows the system appearance.

yarn
yarn example start        # Metro
yarn example ios          # or: android

Troubleshooting

Transitions don't run at all

  • Is <SharedTransitionHost> mounted at the root (and filling the window)?
  • Did you rebuild the native app after installing? (pod install + rebuild)
  • isNativeModuleAvailable() returns false → Nitro autolinking didn't run; check that react-native-nitro-modules is installed in the app.

The element jumps to a wrong final position

  • Use animation: 'fade'/'none' on participating screens — slide_from_right moves the target while it is measured.
  • The wrapper View must not be collapsable (the library sets this for you — don't override nativeID).

Back transition starts from a stale position

  • The reverse animation starts at the element's last measured frame. If the detail screen was scrolled so the element moved, the start frame is stale (known limitation, see docs/api.md).

Text looks stretched mid-flight

  • Use config={{ contentScale: 'transform', crossFade: true }} on text elements.

Caveats (v0.2)

  • Gesture-driven progressive transitions (element follows a swipe-back gesture) are not supported; the reverse animation plays after the pop.
  • borderRadius morphing supports numeric radii only (no percentages / per-corner).
  • Android has been build-verified; runtime QA so far has been on iOS.

Contributing

See the contributing guide. Development log lives in the git history.

License

MIT © Bao Nguyen