Shared Element Navigation

Sharing Elements

The Navigation router lets you smoothly transition between elements that are shared across two scenes when the user navigates from one scene to the other. It's a technique common to native apps, for example, zooming into an enlarged image when a user clicks on a thumbnail in a gallery. To identify elements you want to share across two scenes, you wrap each one in a SharedElement component and give them both the same name. You pass in a prop holding the data you want to animate as one element transitions to the other.

In our email example app, we can smooth out the navigation when a user opens an email by sharing the subject of the email between the 'inbox' scene and the 'mail' scene. We wrap the subject elements on each scene inside a SharedElement. The font size of the subject is smaller in the inbox than on the open email, 14px as compared to 20px. Because we need to scale the text during the navigation, we pass these font sizes, along with the subject line, into the data props.

import {SharedElement} from 'navigation-react-mobile';

// The 'inbox' scene
<SharedElement
  data={{size: 14, subject: mail.subject}}
  name={`mail${mail.id}`}>
  <span>{mail.subject}</span>
</SharedElement>
// The 'mail' scene
<SharedElement
  data={{size: 20, subject: mail.subject}}
  name={`mail${mail.id}`}>
  <span>{mail.subject}</span>
</SharedElement>

Animating

During a navigation, the SharedElementMotion component keeps track of elements that are shared between the two scenes. For each pair it finds, as the navigation progresses, it interpolates between the source and target element screen positions. It calls you back with a style parameter containing these interpolated values so you can animate the shared element. The SharedElementMotion component also accepts onAnimating and onAnimated props so you can style the source and target elements when the navigation starts and finishes.

Returning to the email example, we create a MailShared component that renders a SharedElementMotion component. Inside the child callback function we render the email subject inside a div. We style the div using the interpolated coordinates. To make it look like the subject is moving out of the 'inbox' scene and into the 'mail' scene, we supply an onAnimating prop that hides the subject on these scenes when the navigation begins. An onAnimated prop restores their visibility when the navigation completes.

import {SharedElementMotion} from 'navigation-react-mobile';

const MailShared = (props) => (
  <SharedElementMotion
    onAnimating={(name, ref) => {ref.style.opacity = 0}}
    onAnimated={(name, ref) => {ref.style.opacity = 1}}
    {...props}>
    {(style, name, data) => (
      <div
        key={name}
        style={{
          position: 'absolute',
          fontSize: `${data.size}px`,
          left: data.left,
          top: data.top,
          width: data.width,
          height: data.height,
          transformOrigin: 'top left',
          transform: `
            translate(${style.left - data.left}px, ${style.top - data.top}px)
            scale(${style.width / data.width}, ${style.height / data.height})
          `
        }}>
        {data.subject}
      </div>
    )}
  </SharedElementMotion>
);

We assign our MailShared component to the sharedElementMotion prop of the NavigationMotion component because the shared element animation has to run as part of the overall scene animation.

<NavigationMotion
  unmountedStyle={{opacity: 0}}
  mountedStyle={{opacity: 1}}
  crumbStyle={{opacity: 0}}
  sharedElementMotion={props => <MailShared {...props} />}
  renderMotion={(style, scene, key) => (
    <div
      key={key}
      style={{opacity: style.opacity}}>
      {scene}
    </div>
  )}>

Note

The style parameter of the callback contains the interpolated data props and screen positions. You can directly set them onto the shared element's style props instead of using transform, but the animation might be jankier.