Skip to main content

Arrangement

Two screens as one screen of a stack: side by side on iPad, iPhone Duo open, Android tablets and open foldables, each on its side of the fold, and the main one alone on a phone.

SectionArrangementDraftDocumented, not reviewed yet: the API can change.

A profile and its details, a photo and the facts about it: on a phone the user sees one at a time, but a big screen has room for both. A pair puts a second screen beside the first where the window is large, and leaves the first alone everywhere else. A screen gets its pair with arrangement:

src/screens/Profile.tsx
import { Text } from 'react-native';
import { defineScreen } from '@apexnative/navigation';

function Profile({ id }: { id: string }) {
return <Text>Profile {id}</Text>;
}

export const ProfileScreen = defineScreen({
component: Profile,
options: (s) => s.title('Profile'),
arrangement: { secondary: 'Details' },
});
Navigation.ts
import { createNavigation } from '@apexnative/navigation';
import { DetailsScreen } from './src/screens/Details';
import { HomeScreen } from './src/screens/Home';
import { ProfileScreen } from './src/screens/Profile';

export const Navigation = createNavigation({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
Details: DetailsScreen,
},
root: 'Home',
});
// in Home, on a tap
navigation.push('Profile', { props: { id: '7' } });

On a phone Profile slides in alone, as it would without arrangement. On iPad and on an open iPhone Duo, Details comes in beside it, under Profile's bar; back takes both. This page answers:

  1. What a pair is.
  2. Two ways to make one: on the screen, or under a name of its own.
  3. What each device shows.
  4. Split and overlay, and the props of the second screen.
  5. How the pair lies now, and when the window changes.
  6. The bar of the pair, native screens in a pair, and a pair or a split view.
iPhone Duo

What a pair is​

A pair is one entry of a stack with two screens in it: the primary, the screen the command names, and the secondary, beside it, under it or behind it. The native side lays the two out by the size of the window and by the fold of a device that folds, and the stack treats them as one screen:

[Home] ── push('Profile') ──▶ [Home, Profile + Details]
◀──────── back ────────
  • Both are screens. Each has its own id, props, events and hooks. useNavigation() in either acts on the stack, so a push from Details goes over the pair.
  • The bar is the primary's: its title, buttons, search field and toolbar. See The bar of the pair.
  • They come and go together. The push shows both, back takes both.

iOS 27.1 The pair is UIKit's UIArrangementViewController, which the system lays out on iPhone Duo and iPad. Android A container of the library does the same with the size of the window and the fold of a foldable.

React Navigation
// one screen renders both parts,
// and measures the window itself
function Profile({ route }) {
const { width } = useWindowDimensions();
const { id } = route.params;
return (
<View style={{ flexDirection: 'row' }}>
<ProfileContent id={id} />
{width >= 700 && <Details id={id} />}
</View>
);
}
Here
// two screens; the native side
// places them, at the fold too
defineScreen({
component: Profile,
arrangement: { secondary: 'Details' },
});

React Navigation has no pair: one screen renders both parts and decides by the width. Here the two stay screens of their own, Details can be pushed alone too, and on a device that folds the system puts the boundary on the fold.

Two ways to make a pair​

arrangement of a screendefineArrangement
Shown byThe screen's own name: push('Profile').A name of its own: push('ProfileWithDetails').
SettingsThe secondary, its props, style.Every setting: also axes, fraction, primaryEdge, compact.
A window that isn't largeThe primary alone, a plain screen.The primary alone, or the pair with compact: 'pair'.

On the screen​

arrangement of defineScreen makes a screen the primary of a pair wherever it is shown by its name: push, setStack, replace, a layout, a link. It has no settings to tune: outside a large window the screen is a plain screen, as it is without arrangement, so it works on every device without a second thought. A native screen takes it too:

nativeScreen('Map', { arrangement: { secondary: 'Details' } });

A pair of its own​

defineArrangement pairs two screens of screens under a name of its own, with every setting. The two stay screens of their own: push('Profile') shows Profile as its own definition says, and push('ProfileWithDetails') shows this pair.

Navigation.ts
import {
createNavigation,
defineArrangement,
} from '@apexnative/navigation';
// … the screens

export const Navigation = createNavigation({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
Details: DetailsScreen,
ProfileWithDetails: defineArrangement<{ id: string }>({
primary: 'Profile',
secondary: 'Details',
compact: 'pair', // a phone shows both too
}),
},
root: 'Home',
});
navigation.push('ProfileWithDetails', { props: { id: '7' } });

The type argument is the props of the pair, which the commands check as for any screen. The settings:

  • axes: 'horizontal' side by side only, 'vertical' one above the other only, 'both' by default. Where the window allows neither, only the primary shows.
  • fraction: the primary's share of a split, above 0 and below 1, half by default. At a fold the fold decides.
  • primaryEdge: the side the primary takes. In a split 'leading', the default, puts it first, 'trailing' second; in an overlay that goes side by side, 'trailing' by default.
  • compact: what a window that isn't large shows: 'primary', the default, or 'pair'.

A pair has two screens, never more: in a defineArrangement, the primary's own arrangement doesn't count. Every key: defineArrangement.

What each device shows​

A pair shows both screens in a large window: on iOS a window whose size classes are both regular, on Android a window at least 600 dp wide and 480 dp high, the sizes Material calls neither compact. With arrangement, or compact: 'primary':

DeviceWhat shows
iPhone, portrait and landscapeThe primary alone.
iPhone Duo foldedThe primary alone.
iPhone Duo open; iPadThe pair. An iPad window narrow enough to be compact shows the primary alone.
Android phone, portrait and landscapeThe primary alone.
Android tablet; a foldable openThe pair.
Partly folded in a large window: iPhone Duo, a foldable that opens like a bookThe pair, each screen on its side of the fold.
iOS before 27.1, or an app built with an older SDKThe primary alone.

Where only the primary shows, the secondary is still there, out of the window, and comes in when the window grows, as when the user opens iPhone Duo. Before iOS 27.1 there is no secondary at all: the primary is a plain screen.

With compact: 'pair' of defineArrangement the pair shows everywhere: on a phone held upright, one above the other.

Which screen shows alone, and on which side​

The primary leads the pair: its bar is the pair's, and it is the screen that shows alone outside a large window. To have the other screen alone on a phone or on iPhone Duo folded, make that one the primary. primaryEdge then puts it on the side the layout asks for:

// The list on the leading side, the details on
// the trailing one; folded, the details alone
ItemWithList: defineArrangement<{ id: string }>({
primary: 'Details',
secondary: 'List',
primaryEdge: 'trailing',
}),

The bar stays the details' bar folded and open, so opening iPhone Duo changes nothing in it: the list comes in beside the details, which keep their place in the window. One setting serves both ways a split lies: 'trailing' and 'bottom' put the primary second, at the trailing side or at the bottom; 'leading' and 'top' put it first.

Split and overlay​

style says how the two share the window:

  • 'split', the default: side by side, or one above the other in a tall window; neither covers the other. On a partly folded device each takes one side of the fold.
  • 'overlay': the primary over the secondary, both full size, so the user sees the primary. On a partly folded device they go side by side, the primary at the trailing side.

An overlay fits a screen that fills the window, and whose facts are worth the other half once the device is folded:

export const PhotoScreen = defineScreen({
component: Photo,
arrangement: { secondary: 'Details', style: 'overlay' },
});

Open flat, the photo fills the window. Folded halfway, the photo takes one side and its details the other. On a phone, as with any pair of arrangement, the photo is alone.

iPhone Duo

The props of the second screen​

The props of the command go to both screens. When the secondary takes other props, secondaryProps makes them from those of the show:

export const ProfileScreen = defineScreen({
component: Profile,
arrangement: {
secondary: 'Details',
secondaryProps: (props) => ({ itemId: props.id }),
},
});

push('Profile', { props: { id: '7' } }) gives Profile { id: '7' } and Details { itemId: '7' }. TypeScript checks the props of the command against Profile; what secondaryProps returns isn't checked against Details, so keep the two in line.

The two screens share no React state. Each screen is a React root of its own, so a useState of Profile doesn't reach Details. Give both the same id and read the data from a store or the app's context, as two screens of a stack do (see State management). With appProvider both render inside it, so its providers reach both (see One tree).

How the pair lies now​

A screen of a pair reads how it shows with useArrangement(). Here Profile offers a Details button only where Details isn't on screen already:

src/screens/Profile.tsx
import { Button, View } from 'react-native';
import {
defineScreen,
useArrangement,
useNavigation,
} from '@apexnative/navigation';

function Profile({ id }: { id: string }) {
const navigation = useNavigation();
const pair = useArrangement();
// Details is beside or under Profile
const detailsShown =
pair?.layout === 'sideBySide' || pair?.layout === 'stacked';

return (
<View>
{/* … the profile */}
{!detailsShown && (
<Button
title="Details"
onPress={() =>
navigation.push('Details', { props: { itemId: id } })
}
/>
)}
</View>
);
}

export const ProfileScreen = defineScreen({
component: Profile,
arrangement: {
secondary: 'Details',
secondaryProps: (props) => ({ itemId: props.id }),
},
});

On a phone the button pushes Details; on iPad Details is beside Profile, and the button is gone.

FieldWhat it says
placement'primary' or 'secondary': which screen of the pair this one is.
layout'sideBySide', 'stacked' (one above the other), 'overlay' (one over the other, both full size), or 'single' (one screen in the window).
frontIn an overlay: this screen lies over the other one.
hiddenWith 'single': this screen is the one out of the window.

The hook gives null in a screen outside a pair. Where there is no secondary, before iOS 27.1, the primary gets layout: 'single'. It works in a component of a screen, as the other hooks of a screen do. Every value: useArrangement.

When the window changes​

The user opens iPhone Duo, folds it halfway, makes an iPad window narrower, or unfolds an Android foldable: the pair lays its screens out again, and useArrangement renders with the new state.

  • A secondary that leaves the window disappears, and appears again when it comes back: useScreenDisappear and useScreenAppear run. It stays mounted meanwhile, with its state.
  • The first render already has a state: the one the shape of the window makes likely, side by side in a wide window, one above the other in a tall one, the primary alone outside a large window. The first layout gives the real one, and the screen renders again when it differs.

The bar of the pair​

The stack shows one bar, and in a pair it is the primary's: its title, its buttons, its search field, its toolbar, and the options that change them later (useScreenOptions, setOptions). The system around the screen follows the primary too. What the secondary sets there has no effect:

Options of the secondaryEffect
topBar, toolbarNone: the bar and the toolbar are the primary's.
statusBar, layout.orientationNone: the primary decides.
iOS layout.autoHideHomeIndicator, deferSystemGestures, verticalBarsNone: the primary decides.
Android navigationBarNone: the primary decides.
layout.backgroundColorThe secondary's own background, as on any screen.

A button that acts on Details goes in Profile's options, and reaches Details through the data they share, such as a store.

Native screens in a pair​

Either screen of a pair can be a native screen: nativeScreen takes arrangement as defineScreen does, and primary and secondary can name a native screen. The bar is the primary's, as for React screens.

useArrangement and useReservedRegions are React hooks, so a native screen doesn't get them. iOS Native code asks UIKit: the screen's view controller has arrangementViewController (iOS 27.1), the pair UIKit lays out, and its stateForPlacement: says how it lies. Android A native screen has no state of its pair.

A pair or a split view​

Both put screens side by side on a big screen. They answer different needs:

Split viewA pair
What it isThe columns of the app, each with a stack of its own.One entry of a stack: two screens.
Where it goesThe root: root, roots, switchTo.Wherever a screen is shown by its name: push, a layout, a link.
The barOne for each column.One, the primary's.
BackWithin a column.Takes both screens.
A pickshowInColumn shows it in the column beside.The secondary stays the same: another pick pushes another pair.
A phoneThe compact layout, or the columns as one stack.The primary alone.
iOSUISplitViewController.UIArrangementViewController, iOS 27.1.

The mailboxes and messages of a mail app are a split view: the user picks, and the column beside shows the pick. A profile with its details, pushed from anywhere in the app, is a pair.

A pair that names a screen not in screens​

primary and secondary are names of screens. A name that isn't there, or one that is a defineArrangement itself, stops the start of the app, and the error names the pair:

[navigation] The pair of 'ProfileWithDetails' names 'Detail',
which is not in screens

For a screen that loads later, the check runs when a command shows it, and the command fails. A secondary's own arrangement doesn't count: a pair has two screens, and its secondary shows as a plain screen.

A screen with a key in a pair​

A screen's key is its place in a stack, and in a pair that place is the pair's: the screens of a pair go without their keys. A push of a screen with both key and arrangement shows a new pair each time, and doesn't go back to one in the stack. Where the native side shows no pairs, before iOS 27.1, the primary shows alone and keeps its key, as a plain screen does: a push with the same key goes back to it.

Read next​

API​