Capacitor Foldable

iPhone Duo

iPhone Duo support uses the iOS 27.1 APIs. It turns on when the app uses Capacitor 8.5 or later, is built with Xcode 27.1 or later and runs on iOS 27.1 or later. Apps built with an older Xcode keep working and report no fold. Capacitor 7 apps must stay on Xcode 26: iOS 27 requires the scene lifecycle, which only Capacitor 8.5 and later adopt, so a Capacitor 7 app built with Xcode 27 crashes at launch.

What the plugin reports

Measured in the iPhone Duo simulator from Xcode 27.1 beta:

Pose state · posture Window Size class Hinge Bars
Closed flat · flat, outer display 466 × 678 (678 × 466 sideways) compact width 0° vertical, on the side away from the camera
Fully open flat · flat 951 × 669 (669 × 951 upright) regular 180° vertical in landscape, horizontal upright
Half-open, held sideways half-opened · book 951 × 669, split 456 | 455 regular about 128° vertical
Half-open, held upright half-opened · tabletop 669 × 951, split 456 over 455 regular about 128° horizontal

How much of the screen your app gets

iPhone Duo treats an app by the SDK it was built with: apps older than iOS 27 run in a compatibility mode inside a black border, apps built with iOS 27.0 fill more of the inner display but still leave gaps, and apps built with the iOS 27.1 SDK get the whole display. So npx cap sync ios with Xcode 27.1 matters as much as the code in this plugin.

Apple also requires every app and game uploaded to App Store Connect from April 2027 to be built with the iOS 27 SDK or later, so every app meets the second tier by then whether or not it adapts to the fold.

Keep bars horizontal

Apple’s guidance is to leave this alone: “In general, don’t override the default bar placement. The position of controls on the vertical axis is one of the core patterns of iPhone Duo.” The same page leaves one door open, “Consider using the full display width for interfaces where bars aren’t necessary”, which is where this belongs: games, a camera viewfinder, a drawing canvas, anything immersive that has no system bars to speak of. Reach for it because your layout has no bars, not to avoid adapting one.

When iPhone Duo moves bars to the side, the status bar turns vertical too, and the system adds a leading or trailing safe-area inset for it. To opt out:

  1. In ios/App/App/SceneDelegate.swift, use the view controller the plugin ships:

    import Capacitor
    import FoldablePlugin
    
    window?.rootViewController = FoldableBridgeViewController()
    

    Older Capacitor apps create the controller in Main.storyboard instead. There, set the class to FoldableBridgeViewController with module FoldablePlugin.

  2. Then choose the behaviour from JavaScript, at any time:

    await Foldable.setVerticalBarBehavior({ behavior: 'disabled' });  // everything stays horizontal
    await Foldable.setVerticalBarBehavior({ behavior: 'automatic' }); // back to the system default
    

It resolves to { applied: false } when the app does not use FoldableBridgeViewController, so you can tell the setup is missing. Android, web and iOS before 27.1 always answer false.

The web view’s safe areas follow the bars either way, so you rarely have to do the arithmetic yourself. Measured on the simulator, with the bars on the side the strip arrives as env(safe-area-inset-right) of 84 points on both displays; with disabled, that becomes env(safe-area-inset-top) of 82 and the right inset drops to zero, live and without a reload. getBarPlacement() returns the same width as inset, which is the number to read if you need it in JavaScript rather than CSS.

To move your tab bar to the side instead, like native apps, see Ionic tabs.

Reading the regions yourself

getReservedRegions() returns everything the system reserves on the current display, which on iPhone Duo is the fold, the strip for the vertical status bar and the under-display camera:

const { regions } = await Foldable.getReservedRegions();
// [
//   { kind: 'division', x: 456, y: 0, width: 40, height: 669,
//     margins: { top: 0, right: 20, bottom: 0, left: 20 }, isActive: true },
//   { kind: 'occlusion', x: 677, y: 21, width: 58, height: 37, isActive: false },
//   { kind: 'occlusion', x: 867, y: 0, width: 84, height: 120, isActive: true },
// ]

Straight from UIKit

Writing native code beside the plugin, in a bridge view controller subclass or in SwiftUI, the same regions come from UIView:

let regions = view.reservedRegions(kind: .division, options: .includeInactive)

options: .includeInactive is the part that catches people out. A flat iPhone Duo still has a fold, but the system marks that region inactive, so a plain reservedRegions(kind: .division) returns an empty array until the phone is folded. The same is true of the under-display camera, which is only active while the camera is in use.

Ionic tabs

On iPhone Duo, native tab bars move to the side of the display as a small floating pill under the clock. Ionic’s ion-tabs is HTML, so it stays a full-width bar at the bottom, next to a status bar that has already moved to the side:

Without the plugin With ionic-tabs.css
Ionic tab bar at the bottom of the closed iPhone Duo Ionic tab bar as a pill under the clock on the closed iPhone Duo
Ionic tab bar across the fold in book posture Ionic tab bar as a pill on the side in book posture
Ionic tab bar at the bottom of the open inner display Ionic tab bar as a pill on the side of the open inner display

The plugin ships a stylesheet that turns ion-tabs into that pill wherever iPhone Duo puts bars on the side:

import '@erkamyaman/capacitor-foldable/ionic-tabs.css';
import { installFoldablePolyfill } from '@erkamyaman/capacitor-foldable';

await installFoldablePolyfill({ ionicKeyboard: true });

Angular rejects a CSS import in a TypeScript file, so put the stylesheet in src/global.scss instead:

@use '@erkamyaman/capacitor-foldable/ionic-tabs.css';

The disappearing tab bar

Ionic tab bar missing after folding

Without the plugin, Ionic apps on iPhone Duo can lose their tab bar entirely, as above. Two things cause it:

ionicKeyboard: true takes over from Ionic: keyboard events no longer reach it, and the plugin sets a foldable-keyboard-open class on <html> only while a text field really has the keyboard. ionic-tabs.css hides the tab bar with that class. It’s off by default because it changes which listeners see the keyboard events: the Keyboard plugin’s own Keyboard.addListener still gets them, but window listeners added after the plugin’s don’t.

iOS 26 tab bar

Ionic tab bar as an iOS 26 floating pill on the open inner display

With the inner display upright, iPhone Duo keeps a horizontal tab bar: iOS 26’s floating Liquid Glass pill. Ionic still draws the older full-width bar. To match iOS 26 there too, and on every other iPhone, add:

import '@erkamyaman/capacitor-foldable/ionic-tabs-ios26.css';

This one changes every iPhone running your app, which is why it’s a separate file.

Test on the iPhone Duo simulator

  1. Install Xcode 27.1 or later, then open Settings → Components and download the iOS 27.1 simulator. The iOS 27.2 beta simulator doesn’t run iPhone Duo.
  2. Build your app with that Xcode and run it on an iPhone Duo simulator.
  3. Xcode 27 replaces the Simulator app with DeviceHub. Its controls open, close, half-fold and rotate the phone.
  4. The phone starts closed, so the app appears on the outer display. From a terminal, xcrun simctl io <device> screenshot --display=1 captures the outer display and --display=3 the inner one.

Agent and automation tooling has not caught up with two panels: screenshots come back from the inner panel whatever the fold state, so a folded capture is black, taps do not land, and some tools report the closed display’s coordinates while the phone is open. Three projects have open bugs about it. Drive the simulator from the terminal instead, where --display=1 and --display=3 pick the panel and the hinge CLI sets the angle.

Without the simulator, a browser’s responsive mode at the outer display’s 466 × 678 and the inner display’s 951 × 669 gets you most of the way. Apple’s published inner display size, 626 × 890, is smaller than what the simulator reports, so check both.