DEV Community

Cover image for Building Ionic/Capacitor Apps for the iPhone Duo Era
Masahiko Sakakibara
Masahiko Sakakibara

Posted on Originally published at zenn.dev AI-assisted

Building Ionic/Capacitor Apps for the iPhone Duo Era

iPhone Duo introduces Vertical Bars, moving back buttons and tab bars to the side of the screen.

That changes the layout and the experience more than I expected. How much? App Store screenshot specifications now include iPhone Duo alongside the existing iPhone and iPad requirements. It seems Apple sees this as a distinct experience, too.

As app developers, we need to account for it. So I built a way to adapt an existing Ionic app to Duo. Take a look:

An Ionic app on iPhone Duo switches to Vertical Bars, then uses the native side tab bar and back button.

The switch in this demo enables the vertical layout.

It's the same Ionic app, with its tab bar and back button moved to the side. These aren't HTML and CSS recreations. They're real native UI rendered by UIKit and SwiftUI.

The web screens and application logic stay in place. Native UI handles the navigation controls.

In my previous article (in Japanese), I explored how Duo behaves. This time, I'll show how to bring that behavior to an existing app. This also works with Capacitor apps that don't use Ionic.

How Duo changes navigation and safe areas

Back buttons and tab bars move to the side

On Duo's outer display, and on its inner display in landscape, back buttons and tab bars move into Vertical Bars at the side. Here's the same Ionic app before and after adapting to that layout.

Before After
The same Duo app with navigation buttons at the top and the tab bar at the bottom. The same screen with native navigation controls at the side and content inset from the bars.

Same Duo, same screen. Only the vertical layout has been enabled.

On the right, the buttons and tab bar have moved to the side, and the content stays clear of the bars. Moving the controls also changes how the content beside them fits on screen.

What belongs inside the safe area changes, too

Safe areas need some attention here. This isn't just a matter of moving padding from the top and bottom to the left and right. Compare these two Apple explanations:

Apple's examples: on the left, iPhone X list cell backgrounds extend beyond the safe area; on the right, Duo keeps entire cards and lists inset from the bars.

Left: Building Apps for iPhone X (9:43). Right: Prepare your app for iPhone Duo (6:55).

In the earlier iPhone example on the left, the yellow cell backgrounds extend beyond the safe area, while text and controls stay inside it. In the Duo example on the right, entire cards and lists sit inset from the bars, as shown by the blue region.

The background fills the screen; lists and cards stay inset. That's a change in how we apply safe areas between iPhone and iPhone Duo, and one to keep in mind when adapting an app.

Shared UI makes small differences stand out

Vertical Bars come with many iOS-defined constraints: width, button order, grouping, and animations. You can include custom views, but they need to adapt to the fixed width and vertical layout. Apple's walkthrough explains those rules.

Other apps follow the same rules. That makes small differences easier to notice when we recreate something similar on the web.

I've tried to reproduce iOS 26/27 UI with web technologies, down to the pixel. But did I really match the way Liquid Glass changes shape and animates? That's harder to say.

For UI shared across apps, like Vertical Bars, I wanted to hand that part over to real native controls.

Make just the Vertical Bars native

That's why I built @rdlabo/capacitor-native-ui. It connects native UI with a WebView so they can coexist. Here, we'll use it to rearrange existing navigation buttons and tab bars as native controls for Vertical Bars.

When you use the native tab bar, its actions are passed to the original web components, and your web screens switch as before. The back button works the same way. The appearance and interaction change, while the application logic behind them stays in place.

We can also use the device's layout information to keep the background full-screen and the content inset from the bars. Let's add this to an existing app.

Add it to an existing Ionic app

With the setup below, the plugin reads your existing ion-tab-bar and ion-back-button and renders their native counterparts. Those controls support Duo's Vertical Bars, so you don't need to substantially rebuild your navigation.

npm install @rdlabo/capacitor-native-ui @rdlabo/ionic-theme-ios27 @erkamyaman/capacitor-foldable
Enter fullscreen mode Exit fullscreen mode

Add the vertical-layout CSS to your global Sass so web content stays inset from the bars. You can keep your existing theme.

@use '@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css';
Enter fullscreen mode Exit fullscreen mode

If your page transitions aren't already integrated with native UI, wrap the existing transition animation with withNativeUITransition and assign it to navAnimation before initializing Ionic.

Initialize native UI once for the app. After ion-app is mounted, handle the device's layout information at the app root and start with verticalOnly. Here's that part of the setup:

import { Foldable, type BarPlacement } from '@erkamyaman/capacitor-foldable';
import { setVerticalBarsLayout } from '@rdlabo/ionic-theme-ios27';
import { enableNativeUI } from '@rdlabo/capacitor-native-ui';

const applyPlacement = ({ verticalBarEdge, inset }: BarPlacement) =>
  setVerticalBarsLayout({ edge: verticalBarEdge, inset });

const listener = await Foldable.addListener(
  'barPlacementChange', applyPlacement
);
applyPlacement(await Foldable.getBarPlacement());
const rail = await enableNativeUI({ controls: 'verticalOnly' });
Enter fullscreen mode Exit fullscreen mode

verticalOnly limits native rendering to Vertical Bars. The placement handler reflects the device's bar placement and insets in the web layout, so existing screens adapt to both vertical and regular layouts.

When the component that owns this setup is destroyed, clean up with listener.remove() and rail.destroy(). If you're already initializing native UI, integrate this into that existing setup.

Build the web app, run npx cap sync ios, then build and run the iOS app on Duo. The integration guide includes transition examples and explains where the setup belongs in each framework.

Add it to an app without Ionic

If you don't use Ionic, the plugin can display native buttons in Vertical Bars based on your existing HTML buttons. Here, the native back button at the side returns to a web-based list:

A Capacitor app without Ionic opens a detail screen, then returns to its web list using Duo's native back button on the right.

A plain HTML app on Duo. The back button on the right returns to the web list.

Add these two packages to your existing Capacitor project:

npm install @rdlabo/capacitor-native-ui @erkamyaman/capacitor-foldable
Enter fullscreen mode Exit fullscreen mode

Once the app's root element (#app here) is mounted, initialize once. This is the startup example from the documentation:

import { enableNativeUI } from '@rdlabo/capacitor-native-ui/core';

const shell = await enableNativeUI({
  root: document.querySelector<HTMLElement>('#app')!,
});
Enter fullscreen mode Exit fullscreen mode

Combine this with the documented device-placement integration. Get the initial placement with Foldable.getBarPlacement(), listen for barPlacementChange, and reflect the placement and inset in your existing CSS.

When vertical placement is no longer active, restore the regular spacing and call shell.disableVerticalControlArea(). When it returns, call shell.enableVerticalControlArea(). Process spacing updates and these state changes in sequence.

When the root is destroyed, remove the placement listener and call await shell.destroy().

With initialization in place, add data-* attributes to the existing HTML buttons you want to display as native UI. This marks a back button for placement in Vertical Bars. Pressing it invokes the original click handler.

<button
  type="button"
  data-native-ui="back-button"
  data-native-ui-placement="rail"
>
  Back
</button>
Enter fullscreen mode Exit fullscreen mode

Build the web app, run npx cap sync ios, and rebuild the iOS app. This integration supports buttons and back buttons; it doesn't automatically convert HTML tab bars. The guide and examples cover the setup in more detail.

Help your Capacitor app feel at home on Duo

When someone chooses Duo, we want them to keep using their familiar apps in a way that fits the device.

Keep the web screens and logic you've already built, and use real native UI for navigation. That's now an option for adapting to Duo, too.

The device-placement information comes from Erkam Yaman's Capacitor Foldable. Thank you, Erkam!

@rdlabo/capacitor-native-ui is open source under the MIT license. If you're planning Duo support, give it a try in your existing app!

See you next time.

Originally published in Japanese on Zenn.

Top comments (0)