iPhone Duo, Apple's first foldable iPhone, ships on October 23, and is the first genuinely new iPhone form factor in years. It has a compact outer display for use folded and a large inner display that opens like a book, with a hinge that people hold at any angle, stand on a table, or fold flat.
Expo SDK 58 is built for iOS 27 and iPhone Duo on day one, and with a few changes your app can feel at home on both displays and in every pose.
What’s different?
iPhone Duo adds three major layout concepts to iOS:
- Vertical navigation in most device poses. iOS moves the system status bar, tab bar, and toolbar controls into a vertical rail on one edge, applying when the device is in a book-like orientation. In other poses, iOS keeps horizontal bars.
- New sizes and orientations. iPhone Duo can be opened, partially opened, and rotated, requiring dynamic sizing of your app. Users can also position your app alongside another in Split View.
- Split layouts for iPad-like content arrangement when unfolded. You can choose for a navbar to adapt to a sidebar layout via preferredPlacement, or split content into a two pane view via <ArrangementView>.
Where the system bars go: a rail on the right edge when folded, on the outer edge of each window in Split View, and horizontal bars again on the open display in portrait.
The good news is if you’ve been building your app with Expo Router and <NativeTabs>, many of these concerns are handled for you via the native navigator. The work that remains is auditing your screens and applying additional safe areas, and making decisions for your app’s internal layout.
Additionally, iOS 27.1 provides new APIs for the device hinge, the rail, and reserved regions, all available in SDK 58 and covered further down in this guide.
Requirements
Build with Xcode 27.1 first. It ships the iOS 27.1 SDK that the hinge and reserved region APIs need, and the iOS 27.1 simulator runtime with the iPhone Duo simulator, so you can test without an iPhone Duo. Xcode 27 also replaced Simulator.app with Device Hub (Xcode > Open Developer Tool > Device Hub), which is where the simulator folds, and pressing i in npx expo start boots and focuses it as before. On EAS Build, use the sdk-58 or latest image. An app built with Xcode 26 or earlier runs, but iOS keeps it out from under the rail's status bar and camera, so it does not get the whole display (Preparing your app for iPhone Duo).
High level guidelines
Apple's guidelines for iPhone Duo set the tone for everything below: you are still designing for iPhone. Lay out from the space your view has, treat the folded display as compact width and the open display as regular width, let the layout widen rather than switch designs, and keep the same features and state on both displays. In an Expo app, that comes down to a few choices:
- Let iOS draw the navigation. Expo Router's Stack and NativeTabs, and SwiftUI views through Expo UI's <Host>, use the system's own bars, which iOS places for you on both displays, rail included. A header or tab bar you draw in JavaScript is yours to keep out of the rail. See Expo Router.
- Lay out from the safe area. <SafeAreaView> from react-native-safe-area-context covers 99% of cases. On iPhone Duo the rail is a horizontal inset, on the left in the left Split View window, so pad both sides and let flex take the width. See Safe Areas.
- Let the layout widen, and leave the fold to the system. Alerts, sheets, and system split views move off the fold on their own, and a split view or an ArrangementView gives a list and detail app the two-pane layout when open. Reserved regions and the hinge API are for particular layouts and effects, not for everyday screens. See Additional APIs.
- Test every pose. Fold, unfold, rotate, and enter Split View on the iPhone Duo simulator, and check that a screen stays open with its state as the device folds. See Testing.
Expo Router
Start with navigation, because the one decision that matters most on iPhone Duo is whether the system draws your bars.
Recommended: Use native navigation bars
On iPhone Duo the navigation bar, tab bar, and toolbar are not fixed to the top and bottom. iOS moves them into the rail when the device is folded or open in landscape, back to horizontal bars in portrait, and onto the outer edge of each window in Split View, animating each change and laying the bar items out again as it goes, Back first and the rest in an overflow menu when they do not fit. Bars that UIKit owns get all of this for free. A header or tab bar drawn in JavaScript gets none of it: it stays where you drew it while the rail takes its space beside it, and matching the system means reading the edge yourself, mirroring the item rules, and moving your bar on every fold, rotation, and Split View change. You will have a hard time, and Apple's guidance is to let the system place the bars (Vertical controls).
The same native toolbar and tab bar on the open display in landscape (left) and portrait (right). The system moves them between the rail and the horizontal bars on rotation; bars drawn in JavaScript stay put.
So let iOS draw the navigation. It is the least work and gives the best result on iPhone Duo: Expo Router's Stack and NativeTabs, and SwiftUI views through Expo UI's <Host>, use the system's own navigation and tab bars, which iOS places for you on both displays, rail included. Keep custom chrome inside the content area. A header or tab bar written in JavaScript is a different story: your app draws it, so keeping it out of the rail is your job too. It still works, but takes more care, and a custom header can at least read which edge the rail is on, which Reading the rail's edge covers. The rest of this section covers both.
expo-router 58.0.20 and later also uses the native iOS header appearance by default on iOS 26 and later (#51450). To keep the previous appearance, call enableLegacyHeaderAppearance(). NativeTabs moved to expo-router/native-tabs in SDK 58, so update any import from expo-router/unstable-native-tabs.
Header buttons in the rail
The rail stacks header and toolbar items in one column, Back or Close first, then prominent actions, and spills the rest into an overflow menu from the bottom up. An item needs an icon to go in the rail at all: text-only items stay in a horizontal bar, and custom views are left out (Organize items in your bars). So give each header button an SF Symbol through Stack.Toolbar.Button instead of a custom headerRight component, keep a text label as its child for accessibility and menus, and group secondary actions under a Stack.Toolbar.Menu with the system's ellipsis.circle overflow icon:
Toolbar items with SF Symbols stack in the rail below the back button on the folded display.
JavaScript tab bars
A tab bar your app draws stays where it is drawn. Tabs keeps its bar along the bottom and pads its items by the larger horizontal inset on both sides. Since expo-router 58.0.25, TopTabs lays its tabs out inside the horizontal insets with the background running to the edges, and a plain <TabList> from expo-router/ui pads its content by the horizontal insets it overlaps and nothing else. With asChild, your own component takes over, so wrap the triggers in <SafeAreaView edges={['left', 'right', 'bottom']}> or pad by insets.left and insets.right.
NativeTabs gets the rail with no extra code. Whichever navigator you use, each screen still keeps its own content clear of the rail, which the next section covers.
Split views
Expo Router's SplitView wraps the system split view. Apple recommends a split view when there is enough horizontal width, and says it can have two panes, like Mail, or three, like Keynote (Split views). On the Duo, the split view shows one column when folded and the columns side by side when open. The sidebar and list columns are views in one layout file, and the routes fill the detail column.
A system split view folded, partially open, and open. The columns settle on either side of the fold, and the open message stays open through every pose. SplitView is experimental in SDK 58.
Sidebar or tab bar on the open display
On the open display in landscape, iOS 27 can show NativeTabs as the tab bar in the rail or as a sidebar, the same choice iPad offers. The preferredPlacement prop picks one when only one of them can show: 'sidebar', 'tabBar', or 'automatic', which leaves it to the system. Setting it also turns on sidebarAdaptable. The sidebar comes with a header button that hides and shows it, and folded, where the width is compact, the tab bar shows whatever you ask for.
NativeTabs on the open display with preferredPlacement set to 'sidebar' (left) and 'tabBar' (right).
Safe Areas
<SafeAreaView> from react-native-safe-area-context is your principal API for 99% of cases. Most apps read only the top and bottom insets from it. On iPhone Duo the rail is a horizontal inset. Folded or open, an Expo Router app on the simulator reports the same four insets, and only the window changes:
The status bar is in the rail, so insets.top is 0. A layout that pads only by insets.top, or uses <SafeAreaView edges={['top']}>, runs under the rail.
Apply the left and right insets everywhere you apply top, and list them in every <SafeAreaView edges>:
Easy to miss:
- Inside scroll views: a horizontal carousel or full-bleed row inside a full-width scroll view needs its own horizontal end padding.
- Absolutely positioned views: floating buttons and toasts need right: insets.right + 16, not right: 16.
- Window size: it changes as the device folds and in Split View, where the left window is 469pt wide. Read useWindowDimensions at render time or let flex decide; a value cached when a toast was shown is stale after the window grows.
- Right-to-left languages: the rail stays on the camera's physical edge, so read both left and right rather than assuming a trailing edge.
- The tab bar: inside a NativeTabs screen, insets.bottom includes the tab bar only while the bar runs along the bottom, so do not add a fixed tab bar height to it.
Split View on the open display is the case to test on purpose. Each app gets the rail on its outer edge, so in the left window the rail is on the left: insets.left is 84, insets.right is 0, the window is 469 × 669, and useVerticalBarEdgeIOS() reads 'leading'. A layout that pads only the right edge, or assumes the rail is always trailing, runs under the rail here.
The same mail app in the left window of Split View. The rail moves to the left edge, and the left inset takes the 84pt.
The space beside the rail is asymmetrical by design. Pad the content container by the insets, not the screen's root view: a background or header image can run edge to edge under the rail while scrolling content stays inset, as long as nothing interactive sits there (Vertical controls).
The status bar
The style prop of <StatusBar> still switches the clock in the rail between light and dark, but hidden has no effect: the clock stays and the insets do not change. To keep the status bar and the system bars horizontal on a screen, see Disabling the rail.
Additional APIs
The sections above cover what every app meets. These APIs come up for particular layouts and effects.
Reserved regions
Safe area insets describe the edges. The fold and the cameras sit inside the view, so Apple describes them as reserved regions: a frame to keep content clear of, with an active flag. A division splits the view and an occlusion covers part of it. iPhone Duo has three: the fold, active only while the device is partially open; the outer camera, always active, with the rail laid out around it; and the inner camera, active only while the camera is in use.
The reserved regions on the open display, shaded over a mail app: the fold and the inner camera are inactive while the device is flat and the camera is off, and the status bar's region is active and has grown to take in the Dynamic Island while audio plays.
The system handles most of it: alerts, menus, and sheets move off the fold on their own, and a system split view narrows its columns around it. For your own layout, Apple asks for a container that adapts by itself and an even number of grid columns, so the fold falls between them. One case to handle yourself: a screen that stays one pane as the device opens belongs on the right half, the one that lines up with the outer display, so what a person was looking at stays in place as they unfold (Arrange views in different poses). On iOS 27.1 RC, UIKit parks it on the left.
react-native-reserved-regions, from the maintainers of react-native-safe-area-context, reports the regions to JavaScript in a development build made with Xcode 27.1. Wrap the area you care about in a ReservedRegionsProvider and read useReservedRegions() inside it. Each region has a kind, division or occlusion, and a frame in points relative to the provider, margins included, so laying out up to frame.x clears the fold:
It reports geometry and moves nothing, returns [] until the first measurement, and version 0.2.0 reports active regions only on iOS, so the fold appears while the device is partially open.
Hinge
expo-hinge reports the hinge angle and its status, closed, partiallyOpen, fullyOpen, or unknown, on iOS 27.1 and later and on Android 11 and later foldables, and null everywhere else. Use it for interactions and effects, such as a camera shutter that moves when the device stands half open on a table, and not for layout: the status changes before the system's UI does, so a layout keyed on it moves early, and in the wrong direction when the device unfolds. The window size, safe areas, and reserved regions are for layout.
useHinge() re-renders on every change. Outside React, use getHinge() and addHingeListener(). On Android the status is never closed, so check for an angle near 0 (Status on Android).
The hinge angle and status from expo-hinge on the partially open display, with an ArrangementView putting a panel on each side of the fold.
For a SwiftUI view in Expo UI, the onHingeChange modifier calls your handler with the old and new hinge context, on the UI thread when the handler comes from useWorkletCallback. For a SwiftUI layout that adapts to the fold, ArrangementView places a primary and a secondary view side by side with arrangementViewStyle('split'), or layered with 'overlay', which moves one view to each side of the fold while the device is partially open. Apple's rules for it: keep navigation outside it, and never put it inside a list, scroll view, or split view (Arrangement views).
Reading the rail's edge
Two additions to expo-status-bar in SDK 58 cover what the system leaves to you: a header you draw yourself, and a screen that horizontal bars serve better. Both need iOS 27.1 and a build with Xcode 27.1, and do nothing elsewhere.
The safe area insets say how much space the rail takes, not which side holds the controls. useVerticalBarEdgeIOS() returns 'leading' or 'trailing', and rerenders as the device rotates or Split View moves the app to the other side. It is null wherever the system draws no rail: the open display in portrait, other iPhones, and iOS before 27.1. The value is UIKit's verticalBarEdge trait, so a custom header can stack its buttons along the same edge the system uses:
Outside a component, StatusBar.getVerticalBarEdgeIOS() returns the same value once. Like the trait, both keep reporting an edge while your app has disabled the rail, so check the insets if you need to know whether the rail is there.
Disabling the rail
<StatusBar verticalBarBehaviorIOS="disabled" /> keeps the status bar and the system bars horizontal while it is mounted, the layout every other iPhone has, and the rail returns when it unmounts. iOS animates the change and adds or removes the rail's safe area inset with it. Apple reserves this for screens that horizontal bars serve better, such as a full-screen video player with its controls or a fixed layout like a calculator, and asks that it be a stable choice for a screen rather than something toggled with the view's state (preferredVerticalBarBehavior):
StatusBar.setVerticalBarBehaviorIOS('disabled') is the imperative form, and 'automatic' restores the default.
This is the layout Safari uses on the open display, with its controls along the top and bottom. On the simulator, disabling the rail moves the status bar and header back to the top and the tab bar to the bottom, folded and open alike, and the safe area follows. The open display in portrait is always in this state, whatever the app asks for, so a screen that handles disabling the rail has handled portrait too.
The open display with the rail (left) and with the vertical bar disabled (right).
Limitations
- Orientation locks are a preference on iOS 27. Wherever an iPhone app can be resized, in iPhone Mirroring or on iPad, the system treats a supported orientation as a preference, lockAsync can be refused, and requireFullScreen no longer opts your app out (Orientation locks on iOS 27 and later). On iPhone Duo itself, Apple says you can lock to portrait or landscape, as long as you fill the screen as the pose changes (Best practices).
- JavaScript tab bars can draw under the rail. Expo Router's JavaScript top tabs and TabList from expo-router/ui don't pad for the horizontal safe area before expo-router 58.0.25, which fixes both (#51814, #51813). Update to it, wrap custom bars in SafeAreaView with the left and right edges, or use NativeTabs.
- Split View and posture changes are your layout's job. SDK 58 makes Expo packages size themselves from the window and gives you ArrangementView and expo-hinge, but it does not lay out your screens for you.
- The iPhone Duo simulator has gaps. Xcode 27.1's release notes list StandBy and most app extensions as unavailable in the iPhone Duo simulator runtime, so check widgets and other extensions on a device.
Testing
Test on the iPhone Duo simulator in Xcode 27.1, folded and open, and in each orientation your app supports. Device Hub folds the simulator to any angle and shows the display the device is using. Walk every screen in each pose, and rotate the device in each pose:
- Folded: nothing important sits under the rail, and every button responds. Header buttons you expect in the rail are there, with the rest in the overflow menu.
- Open: the layout does not jump to a different design, and nothing runs under the rail. In portrait, the bars are back at the top and bottom and the layout still holds.
- Partially open: nothing you need to read or tap sits in the fold, and sheets and popovers land on one side of it.
- Split View: with your app in each window on the open display, the content stays clear of the rail, which sits on the left edge of the left window.
- Folding with a screen open: the screen stays open at the same scroll position, with the same state and the same actions on both displays.
- A standard iPhone: nothing changes.
Other iOS 27 work in SDK 58
- App Intents. expo-app-intents exposes your app's actions and content to Siri, Shortcuts, Spotlight, and Apple Intelligence. The library is in alpha. Some schemas need a newer iOS version than their domain, and .mail.openDraft needs iOS 27. The SDK 58 post walks through it.
- Device Hub. Expo CLI boots simulators through Xcode 27's Device Hub, and expo-device-hub gives you a browser dashboard for them (Device Hub support for Xcode 27).
Further reading
We recommend that you point a coding agent at this blog post, have it open a PR with the inset fixes, and run the build on the iPhone Duo simulator through Expo CICD Workflows before you merge. Let us know how that goes. Obviously having the Expo MCP connected will be helpful as well. And here are some other helpful resources for iPhone Duo design and development best practices.
- Designing for iPhone Duo in the Human Interface Guidelines, for the design rules behind this guide: the rail, reserved regions, arrangement views, and device poses.
- Preparing your app for iPhone Duo, Apple's developer overview, including the rules for which bar items the rail shows.
- Apple's tech talks: Prepare your app for iPhone Duo, Raise the bar with iPhone Duo, Strike a pose with adaptive layouts on iPhone Duo, and Design for iPhone Duo.
- Expo docs: Safe areas, Native tabs, Stack toolbar, expo-hinge, and ArrangementView.











