Skip to main content

Transitions

How screens come and go: the platform's transitions, presets for a command, transitions out of a view (shared elements, zoom, preview), animations of your own, and waiting for the content. Which to pick, and what each costs.

SectionTransitionsDraftDocumented, not reviewed yet: the API can change.

A transition is how a screen comes in and goes. By default it is the platform's own, with its gestures. A command can ask for another one with transition:

import { navigation } from '@apexnative/navigation';

navigation.push('Details', { transition: 'fade' });

Details fades in over the screen before it, and fades out on the way back. This page answers:

  1. The platform's transitions, and why they are the default.
  2. Presets for a command, or for the whole app.
  3. Transitions out of a view: shared elements, zoom, preview.
  4. Animations of your own.
  5. Which to pick.
  6. Waiting for the content.

The platform's transitions​

Without a transition, every move is the platform's: a push slides in from the edge, a sheet rises from the bottom, a new tab shows at once. They follow the finger: the swipe back of iOS moves the screen as the user drags, and the user can stop halfway. They also follow the system: on Android a push plays the transition the system gives to a move between screens.

So the platform's transitions are the right choice for most moves. A preset or an animation of your own is for a move that means something else: a modal that should fade, a screen that grows out of a photo.

Presets​

transition of push and showModal takes a preset. The way back is the same in reverse:

iOS
Android
PresetThe screen
'platform'The platform's own transition: the default.
'fade'Fades in, and out on the way back.
'slideUp'Comes from the bottom, goes down.
'slideFromEnd'Comes from the trailing edge: the right, or the left in a right-to-left app.
'scale'Grows in place, and shrinks on the way back.
'none'Shows at once.

A preset takes 300 ms. { type, duration } sets another time, in milliseconds:

navigation.showModal('Player', {
transition: { type: 'slideUp', duration: 450 },
});

The top bar of a pushed screen goes with the screen on Android and stays in place on iOS: see the bar in a transition of your own.

presentation of createNavigation gives every push or modal of the app its transition: see Defaults for the whole app. New roots have presets of their own: see How a new root comes in.

Transitions out of a view​

Some moves start from a view: a photo opens out of its tile, an avatar grows into a profile. The view on the screen has a nativeID, and the transition names it:

iOS
Android
iOS

Shared elements​

Views move from one screen into the other. Each pair names a view of this screen and a view of the next; the rest of the screen fades. Back moves them home.

src/screens/People.tsx
<Image nativeID={`avatar-${person.id}`} source={person.photo} />
<Text nativeID={`name-${person.id}`}>{person.name}</Text>
src/screens/Person.tsx
<Image nativeID="person-avatar" source={photo} />
<Text nativeID="person-name">{name}</Text>
navigation.push('Person', {
props: { id: person.id },
transition: {
shared: [
[`avatar-${person.id}`, 'person-avatar'],
[`name-${person.id}`, 'person-name'],
],
},
});

The avatar of the row moves and grows into the photo of the profile, and the name into its larger title. The views can differ in size and style. A pair that isn't found is skipped (the transition waits for it first); one that fails on the way is skipped with a warning in the log. Either way its views show with their screens, and the other pairs move. type and duration next to shared change how the rest of the screen comes, 'fade' by default.

A view turned or scaled by its own transform, or by a parent's, flies from where it shows and lands as the other view shows, turning the shorter way. The angle is the one React Native draws, from −180° to 180°: rotate: '270deg' turns back a quarter, and no pair spins a full turn; a spin is an animation of the view's own.

What flies is a copy: pictures of the two views, taken as the transition starts, while the views wait hidden in their places. Both platforms. Android The next screen waits until it is drawn, so its views are there to take pictures of; an image flies as its own image, from its fit in one view to its fit in the other; a type other than a fade gives way to a fade. A skew and a tilt (skewX, rotateX, …) don't fly: the view flies without them and has them again at the end.

Zoom​

iOS 18+ The next screen opens out of a view and shrinks back into it, as photos do in the system apps. The swipe back, and the swipe down of a sheet, shrink it with the finger:

navigation.push('Photo', {
props: { id: photo.id },
transition: { zoom: `photo-${photo.id}` },
});

The tile has the same nativeID: photo- and the id of the photo. showModal takes the zoom too. On Android and older iOS the screen comes in with the platform's transition.

Preview​

iOS A long press on a view shows the screen it opens, small, with a menu under it; a tap on the preview pushes the screen. A screen says which views preview what with usePreview, by the view's nativeID:

src/screens/Photos.tsx
import { Image, Share } from 'react-native';
import { usePreview } from '@apexnative/navigation';

function Tile({ id, uri }: { id: string; uri: string }) {
usePreview(`photo-${id}`, {
screen: 'Photo',
props: { id },
actions: [
{
id: 'share',
title: 'Share',
onPress: () => Share.share({ url: uri }),
},
],
});
return <Image nativeID={`photo-${id}`} source={{ uri }} />;
}

The previewed screen renders when the press starts, not when the component mounts (when the screen renders). The preview goes when the component unmounts. On Android a long press on the view does nothing of the navigation's: the view has no preview.

Animations of your own​

options.animations describes a move property by property: the screen that comes in, and the one that goes. It is for a move a preset doesn't have, and it goes over the transition of the command:

navigation.push('Details', {
options: {
animations: {
push: {
content: {
enter: {
translationY: { from: 300, to: 0, duration: 300 },
alpha: { from: 0, to: 1, duration: 300 },
},
},
},
},
},
});

Details rises 300 points as it fades in. Each property animates from a value to a value over a time, with a delay and an interpolation if it needs them; the API lists every one.

iOS Once the move starts, the system draws it on its own, as it draws UIKit's animations: work on the main thread, such as a screen that mounts a long list, doesn't stop it.

The same goes for pop, showModal, dismissModal, setStackRoot and setRoot. The swipe back is the exception: iOS by default it follows the finger with the system's slide, so a custom pop plays for the back button and pop() only. Android A pop with an animation of its own shows no preview under the swipe: the screen pops when the swipe ends.

The swipe back with a pop of your own​

iOS A screen that leaves with a move of its own, such as a card that shrinks away, looks wrong when the swipe back slides it out like any other screen. interactive on its pop plays its own move under the finger instead:

navigation.push('Details', {
options: {
animations: {
pop: {
interactive: true,
content: {
exit: {
scaleX: { from: 1, to: 0.8, duration: 400 },
scaleY: { from: 1, to: 0.8, duration: 400 },
alpha: { from: 1, to: 0, duration: 400 },
},
},
},
},
},
});

The swipe from the edge now shrinks Details as far as the finger has gone across the screen. The user can stop halfway; letting go past half way, or a flick back, finishes the pop, and letting go before half way, or a flick the other way, grows the screen back. After a quick flick the rest plays faster, never slower. The elements and the shared elements of the pop move with it, so a shared element flies home under the finger.

  • It applies only to a pop with an animation of its own, and transition presets get it through options.animations.pop.interactive.
  • The swipe moves the time of the animation, not a point: a move with an easing doesn't stay exactly under the finger.
  • On iOS 26 the swipe from the content stays off for a screen with a pop of its own; the swipe from the edge works.
  • The top bar shows the screen under from the start of the swipe, as it does in any move of your own, and comes back if the user gives the swipe up.
  • While the finger is down, the system's animations inside the two screens, such as an activity indicator, hold still.

Android It doesn't apply: a pop with an animation of its own plays when the back gesture ends.

The top bar​

The top bar goes its platform's way in a preset or an animation of your own. Android It goes with its screen, as if it were part of it: with slideUp the new screen comes up with its bar over the old one and its bar, and on the way back it goes down with its bar, uncovering the bar below. With fade or scale the bar fades or grows with its screen. iOS UIKit keeps the bar in place and changes its title and buttons as the transition starts; only the screens under it move.

When the user reduces motion​

The system's setting to reduce motion wins over a move of your own, with nothing to set. iOS With Reduce Motion on, the move becomes a fade of the same length: the screen that comes fades in on a push, a modal's show and setRoot, and the screen that goes fades out on a pop and a dismissal. No element moves. Android With Remove animations on, every animation of the app ends at once: the move jumps to its end, no element moves and the push doesn't wait for one, a side menu opens and closes without its slide, and a sheet shows and closes without it.

Which to pick​

The movePickWhat it costs
Most movesThe platform'sNothing: it follows the finger and the system.
A move that means something else, such as a fading modalA presetThe swipe back on iOS still slides, unless the pop is interactive.
A view that becomes the next screenShared elements, or the zoom on iOS 18+A nativeID on both views. The zoom shows on iOS 18+ only.
A look at a screen before opening itThe previewiOS only.
A move no preset hasoptions.animationsEvery property spelled out; the swipe back on iOS still slides, unless the pop is interactive.

Waiting for the content​

A screen can come in before its React content has drawn, or wait for it. Coming in at once keeps the move fast; waiting keeps the user from seeing an empty screen slide in.

By default
PushWaits for the screen's first mount, half a second at most, so it comes in drawn, with what it set as it rendered (a title from useScreenOptions) already in its bar. A native screen comes in at once. iOS The components of the bar wait too, each to its own limit from the start of the wait. Android The components of the bar draw as the screen comes in; with appProvider the screen is rendered before the push starts (below).
Android modalWaits for the content and the components of the bar, half a second at most, so the screen comes in drawn.

waitForRender of an animation changes it. true waits as long as it takes; false comes in at once, the content drawing as it comes (a push on Android with appProvider still renders the screen first):

navigation.push('Details', {
options: { animations: { push: { waitForRender: true } } },
});

A screen that draws nothing at first holds its transition back half a second at most, then comes in with its bar.

A push on Android, in one tree​

Android With appProvider the screen comes in drawn without a wait of its own: nothing of it mounts during the transition, which would drop frames. It goes in three steps:

  1. The native side lays the screen out in its stack, out of sight, and gives React its size and its safe area.
  2. React renders the screen at that size with its content, at once.
  3. The push starts right after that content appears, once what the screen set as it rendered (its useScreenOptions) is applied; if it doesn't appear, after half a second.

So the push starts later by the time React takes to render the screen, as a push in a native app starts once its screen is built. waitForRender: false goes the same way here; true waits for the content as long as it takes, and for the components of the bar, half a second at most.

The size and the safe area come from the options the screen is pushed with. Options the screen changes as it renders for the first time, such as hiding the top bar or its safe area edges, apply as it comes in, and its content lays out once more during the transition. Put such options in the screen's own options instead.

API​