L’iPhone pliable Apple Duo change la façon dont fonctionnent les éléments système et les zones sûres. Expo a publié un guide pratique pour les apps React Native sur SDK 58. Cet article est l'équivalent Capacitor : une WebView, des insets CSS env, les plugins Capacitor essentiels, et des barres natives optionnelles de @capgo/capacitor-native-navigation.
Ce qui change sur l'iPhone Duo
Dans de nombreux cas, iOS déplace la barre d'état, la barre de navigation et la barre d'onglets dans une rail latéral vertical sur le bord avant ou arrière. Les barres horizontales apparaissent toujours en mode portrait sur l'écran interne ouvert et dans les mises en page de téléphone classiques.
L'image ci-dessus montre l'écran ouvert. En paysage, la barre d'outils et la barre d'onglets sont empilées dans le rail. En portrait, la barre d'outils revient en haut et la barre d'onglets en bas.
Pour le contenu web à l'intérieur d'une WebView Capacitor, les changements importants sont :
env(safe-area-inset-top)peut être0quand la barre d'état se trouve dans le rail, et non sous l'encoche.- Les insets avant et arrière augmentent (Apple documente de l'ordre de 84 pt pour le rail en paysage et en posture pliée). La gauche et la droite ne sont pas symétriques.
- Changements de taille de fenêtre lorsque l'utilisateur plie, déplie, fait pivoter l'appareil ou utilise Split View sur l'écran interne. Les valeurs de layout en cache deviennent rapidement obsolètes.
- Les barres d'onglets natives suivent le rail lorsque vous utilisez
UITabBar(ou Capgo Native Navigation avec la barre d'onglets système). Les barres d'onglets HTML fixes personnalisées ne le permettent pas.
Considérez l'écran externe plié comme ayant une largeur compacte et l'écran interne ouvert comme ayant une largeur régulière. Élargissez les layouts au lieu de basculer vers un autre app shell.

Où se placent les barres système : un rail à droite lorsque l'appareil est plié, sur le bord extérieur de chaque fenêtre en Split View, et de nouveau des barres horizontales sur l'écran ouvert en portrait.
Prérequis
- Capacitor 8 sur iOS 27 (Xcode 27.1 avec le runtime du simulateur iPhone Duo).
- Device Hub (Xcode > Open Developer Tool > Device Hub) pour plier, articuler et faire pivoter le simulateur Duo.
- Un viewport qui active les safe areas (voir ci-dessous).
viewport-fit=cover et les safe areas CSS
Les variables d'environnement safe area ne s'appliquent que si le viewport couvre tout l'écran :
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Pad interactif contenu par bord. Laissez les arrière-plans s'étendre bord à bord pour plus d'immersion.
Choisissez un responsable de l'inset pour toute l'app. Les deux extraits ci-dessous sont des alternatives, et non une paire à combiner.
Option A : shell par page
.page {
min-height: 100dvh;
padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
}
Ignorez l'option B si vous utilisez l'option A.
Sur les vues scrollables, appliquez l'inset au contenu, pas seulement le conteneur de scroll externe, pour que les arrière-plans défilent sur toute la largeur tandis que texte et boutons restent à l'écart du rail.
Utiliser quatre valeurs indépendantes. Ne supposez pas padding-left égal à padding-right. En RTL, la barre reste du côté de la caméra ; lisez les deux left et right valeurs env.
Deux endroits sont faciles à rater :
- Un scroller horizontal dans une page déjà espacée nécessite son propre padding de fin. Sinon, la dernière carte glisse sous la barre de navigation.
- Un bouton flottant configuré sur
right: 16pxignore la zone de sécurité. Ajoutez l'inset gauche ou droit en direct, et recalculez-le quand la fenêtre change de taille.
Pour les apps Tailwind, @capgo/tailwind-capacitor expose pt-safe, px-safe, et les utilitaires associés au lieu de répéter env() partout.
Capacitor System Bars (safe area sur Android et config sur iOS)
Capacitor 8 est disponible Barres système dans @capacitor/core. Il remplace l'ancien flag réservé à Android adjustMarginsForEdgeToEdge . Configurez-le dans capacitor.config.ts:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
plugins: {
SystemBars: {
insetsHandling: 'css',
},
},
};
export default config;
insetsHandling: 'css'(par défaut sur Android) : injecte--safe-area-inset-*Variables CSS lorsque les valeurs de l'environnement WebView sont incorrectes (anciennes versions d'Android WebView).- L'option B (shell global) utilise le modèle de repli issu de la documentation System Bars. Ignorez l'option A si vous utilisez l'option B.
html {
padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
}
À l'exécution, vous pouvez styler ou masquer les barres avec SystemBars.setStyle, SystemBars.show, et SystemBars.hide depuis @capacitor/core. Il n'existe pas de @capacitor/safe-area package : les safe areas des apps Capacitor sont les env CSS, variables System Bars sur Android, ainsi que vos propres listeners sur resize / visualViewport.
@capacitor/status-bar
Le Le plugin Status Bar contrôle toujours l'overlay et le style sur iOS :
import { StatusBar, Style } from '@capacitor/status-bar';
await StatusBar.setOverlaysWebView({ overlay: true });
await StatusBar.setStyle({ style: Style.Dark });
setOverlaysWebView({ overlay: true }) permet au HTML de s'afficher sous la zone de la barre d'état. Associez-l’à un padding env, sinon le contenu sera rogné. Sur iPhone Duo, le mode overlay s'applique toujours ; la géométrie dont l'inset (haut ou côté) provient de la safe area système, pas seulement des API Status Bar.
Ne cumulez pas les marges automatiques de la WebView, padding d'environnement plein écran, et variables d'inset de la barre native sans stratégie explicite. Ce schéma provoque un double espacement sur chaque iPhone, et le problème s'aggrave lorsque le rail déplace les insets sur les côtés.
contentInset du WebView iOS
Dans capacitor.config.ts, ios.contentInset correspond à la vue de défilement de la WebView contentInsetAdjustmentBehavior. De nombreuses applications utilisent 'never' lorsqu'elles gèrent les insets en CSS. Si vous vous fiez à l'ajustement automatique d'UIKit et padding CSS env, choisissez une seule source de vérité.
Consultez Config iOS Capacitor.
Redimensionnement du WebView lors du pliage/dépliage
Capacitor conserve une seule WebView plein écran. Lorsque le Duo change de posture :
- UIKit met à jour les insets de la safe area du view controller.
- The WebView layout pass runs with a new width and height.
- CSS
env(safe-area-inset-*)et100dvh/visualViewportles valeurs se mettent à jour au prochain style flush.
Écoutez les changements de géométrie en JavaScript lorsque vous mettez en cache des positions dépendantes des insets (boutons flottants, toasts, overlays canvas) :
function onGeometryChange() {
const vv = window.visualViewport;
const width = vv?.width ?? window.innerWidth;
const height = vv?.height ?? window.innerHeight;
const offsetLeft = vv?.offsetLeft ?? 0;
const offsetTop = vv?.offsetTop ?? 0;
document.documentElement.style.setProperty('--app-vv-width', `${width}px`);
document.documentElement.style.setProperty('--app-vv-height', `${height}px`);
document.documentElement.style.setProperty('--app-vv-offset-left', `${offsetLeft}px`);
document.documentElement.style.setProperty('--app-vv-offset-top', `${offsetTop}px`);
}
window.addEventListener('resize', onGeometryChange);
window.visualViewport?.addEventListener('resize', onGeometryChange);
window.visualViewport?.addEventListener('scroll', onGeometryChange);
onGeometryChange();
Le listener de scroll couvre les déplacements du viewport visuel qui changent offsetLeft / offsetTop sans redimensionner la largeur ou la hauteur (par exemple les décalages de viewport dus au clavier sur iOS).
Sur Android, incluez density dans android:configChanges sur votre activité principale afin que les pliables et les changements de taille d'écran ne détruisent pas la WebView. Capacitor 8 templates documentent cela dans le Capacitor guide de mise à niveau.
Orientation et classes de taille de fenêtre
Utiliser live largeur au layout de la branche, pas screen.width capturée une seule fois au démarrage :
const compactQuery = window.matchMedia('(max-width: 600px)');
function readCompactWidth() {
return compactQuery.matches;
}
compactQuery.addEventListener('change', () => {
document.documentElement.dataset.widthClass = readCompactWidth() ? 'compact' : 'regular';
});
document.documentElement.dataset.widthClass = readCompactWidth() ? 'compact' : 'regular';
En CSS, privilégiez flex et grid avec minmax() plutôt que des 100vw largeurs fixes. 100vw ignore souvent les barres de défilement et l'asymétrie du rail, provoquant un débordement horizontal.
Pour le Split View sur l'écran interne, chaque fenêtre peut être étroite (Apple cite environ 469 pt de large). Testez votre app ancrée à gauche et à droite : le rail s'attache à la fenêtre edge, ce qui peut inverser les insets horizontaux d’une fenêtre à l’autre.
Habillage HTML personnalisé vs barres natives
Le guide d’Expo recommande le natif Stack et NativeTabs afin que iOS gère le placement des rails. La même logique s’applique dans Capacitor :
| Approche | Comportement d’iPhone Duo |
|---|---|
| En-tête HTML fixe + onglets en bas | Reste dans les rectangles de sécurité classiques haut/bas. Ne passe pas dans le rail. |
@capgo/capacitor-native-navigation avec le système UITabBar / UINavigationBar |
Les barres suivent le placement du système, rail vertical inclus. |
Si vous devez conserver l'interface HTML, auditez chaque écran pour gauche et droite et pas seulement en haut et en bas.

L'interface HTML fixe ne suit pas la barre latérale. Elle reste dans les zones de sécurité en haut et en bas. Les barres natives, dessinées par UIKit ou par @capgo/capacitor-native-navigation, se déplacent avec le système.
@capgo/capacitor-native-navigation sur iPhone Duo
Native Navigation dessine de vraies UINavigationBar et UITabBar (ou des barres Material sur Android) par-dessus une seule WebView Capacitor. Sur iOS 27 avec iPhone Duo, system tab bars move into the side rail en mode plié et dans de nombreuses positions paysage. La courbe la forme de la barre d'onglets reste sur le bord inférieur. L'action centrale conserve un socle circulaire tandis que la fenêtre s'élargit.

À gauche, la barre d'onglets système est dans le rail. À droite, la barre d'onglets incurvée reste en bas et l'action centrale garde son socle.
Configuration unique
import { NativeNavigation } from '@capgo/capacitor-native-navigation';
await NativeNavigation.configure({
contentInsetMode: 'css',
});
contentInsetMode: 'css' writes bar and safe overlap sizes to the document root:
--cap-native-navigation-top--cap-native-navigation-right--cap-native-navigation-bottom--cap-native-navigation-left--cap-native-navbar-height--cap-native-tabbar-height--cap-native-system-tabbar-height(hauteur nominale de la barre d'onglets quandtabbarHeightest0pour les barres système hébergées)
Lorsque Native Navigation gère les barres, ajoutez du padding avec les variables du plugin ci-dessous. Elles incluent déjà le chevauchement de la barre native et les bords de la safe area UIKit lorsqu'une barre est active. Utilisez max() avec des fallbacks d'env pour que les bords restent corrects lorsqu'une variable est 0 (par exemple les barres d'onglets système hébergées) sans ajouter le padding du plugin et de l'env ensemble.
main {
padding-top: max(var(--cap-native-navigation-top, 0px), env(safe-area-inset-top, 0px));
padding-right: max(var(--cap-native-navigation-right, 0px), env(safe-area-inset-right, 0px));
padding-bottom: max(var(--cap-native-navigation-bottom, 0px), env(safe-area-inset-bottom, 0px));
padding-left: max(var(--cap-native-navigation-left, 0px), env(safe-area-inset-left, 0px));
}
N'appliquez pas non plus le padding de l'option A ou de l'option B sur le même main élément.
Sur iOS 26+ avec une barre d'onglets barre d'onglets, tabbarHeight les insets du plugin peuvent être 0 so web content is not lifted twice. Use --cap-native-system-tabbar-height lorsque vous avez besoin d'espace pour des actions flottantes au-dessus du rail.
safeAreaChanged
S'abonner lors d'un changement de visibilité du pli, du rail ou de la barre :
await NativeNavigation.addListener('safeAreaChanged', ({ insets }) => {
console.log(insets.top, insets.right, insets.bottom, insets.left);
console.log(insets.navbarHeight, insets.tabbarHeight, insets.systemTabbarHeight);
});
Le plugin émet également capNativeNavigation:safeAreaChanged sur window avec le même insets payload.
Native code applique un petit espace en haut quand safeInsets.top est à zéro (barre d'état dans le rail) pour que les boutons de la navbar ne touchent pas le coin arrondi de l'écran. Votre padding web doit toujours utiliser env left/right pour le rail.
setNavbar, setTabbar, et configure renvoient tous { insets } lorsqu'une lecture ponctuelle est nécessaire après la mise à jour de Chrome.
Référence complète de API : Documentation Native Navigation.
Checklist de test
Dans Device Hub avec le simulateur iPhone Duo :
- Replié: les contrôles ne sont pas sous le rail ; les taps sont enregistrés ; les insets horizontaux sont non nuls là où attendu.
- Ouvert en portrait: les barres haut/bas reviennent là où iOS les place ; le layout ne bascule pas vers une autre architecture de l'information.
- Ouvert en paysage / rail: le contenu évite la barre latérale ; les actions flottantes utilisent
insets.rightouinsets.left, et non une valeur codée en dur16px. - Pliage partiel: aucune UI critique dans la zone de la charnière ; les modales et les sheets restent utilisables.
- Split View: lancez l'app dans des fenêtres gauche et droite ; le bord du rail suit chaque fenêtre.
- Repliage avec un écran ouvert: route state, scroll position, and form input survive the transition.
- Simulateur iPhone classique: aucune régression.
Prompt de l'agent (copier-coller)
Utilisez ce bloc avec Claude Code, Cursor, Codex ou un autre agent de code travaillant sur un repo Capacitor :
Task: Make this Capacitor app safe for iPhone Duo (iOS 27 foldable).
Context:
- Read https://capgo.app/blog/capacitor-iphone-duo-safe-area-guide/ and Expo's https://expo.dev/blog/supporting-iphone-duo for platform behavior.
- One WebView. Use viewport-fit=cover and per-edge env(safe-area-inset-*) padding for interactive content; backgrounds may ignore safe areas.
- Capacitor 8: configure plugins.SystemBars (insetsHandling css). Set viewport-fit=cover in index.html. Use var(--safe-area-inset-*, env(...)) fallbacks on Android.
- @capacitor/status-bar: only if the app already uses it; setOverlaysWebView must match CSS padding strategy.
- Do not double-apply Capacitor margin adjustment, full-page env padding, and native navigation CSS variables.
- On resize and visualViewport resize, recompute any cached inset or FAB positions.
- If the app uses @capgo/capacitor-native-navigation: contentInsetMode css, listen for safeAreaChanged, pad with max(var(--cap-native-navigation-*, 0px), env(safe-area-inset-*, 0px)) per edge (not sum); verify system tab bar rail poses in the iOS 27 Duo simulator.
- If the app uses custom HTML headers or tab bars, add left/right safe padding on every screen and document that bars will not move into the rail.
Deliverables:
1. List files changed and the inset strategy (single source of truth).
2. Replace fixed top-only safe padding with four-edge padding where needed.
3. Add or update capacitor.config and index.html viewport meta if missing.
4. Note manual test poses: folded, open portrait, open landscape rail, Split View left/right.
Constraints:
- Only use APIs that exist in @capacitor/core, @capacitor/status-bar, and @capgo/capacitor-native-navigation definitions.
- No em dashes or en dashes in user-facing strings you add.
Installer Skills Capgo pour que les agents sachent comment connecter Native Navigation : npx skills add Cap-go/capgo-skills --skill capacitor-plugins.