L'iPhone Duo pieghevole di Apple cambia il funzionamento del system chrome e delle safe area. Expo ha pubblicato una guida pratica per le app React Native su SDK 58. Questo post è l'equivalente Capacitor: una WebView, CSS env insets, Capacitor plugin core e barre native opzionali da @capgo/capacitor-native-navigation.
Cosa cambia su iPhone Duo
In molte pose, iOS sposta la status bar, la navigation bar e la tab bar in una barra laterale verticale sul bordo leading o trailing. Le barre orizzontali compaiono ancora in portrait sul display interno aperto e nei layout classici da smartphone.
L'immagine sopra mostra il display aperto. In landscape, la toolbar e la tab bar si impilano nella barra laterale. In portrait, la toolbar torna in alto e la tab bar torna in basso.
Per i contenuti web all'interno di una WebView Capacitor, i cambiamenti principali sono:
env(safe-area-inset-top)può essere0quando la status bar si trova nella barra laterale, non sotto il notch.- Gli inset leading e trailing aumentano (Apple documenta nell'ordine di ~84pt per la barra in landscape e nelle pose piegate). Sinistra e destra non sono speculari tra loro.
- Modifiche alla dimensione della finestra quando l'utente piega, apre, ruota o usa Split View sul display interno. I valori di layout in cache diventano obsoleti rapidamente.
- Le tab bar native seguono la rail quando usi
UITabBar(o Capgo Native Navigation con la tab bar di sistema). Le tab bar HTML fisse personalizzate no.
Considera il display esterno piegato come larghezza compatta e il display interno aperto come larghezza regolare. Amplia i layout invece di passare a un altro app shell.

Dove si trovano le barre di sistema: una rail a destra quando il dispositivo è ripiegato, sul bordo esterno di ogni finestra in Split View e barre orizzontali di nuovo sul display aperto in verticale.
Requisiti
- Capacitor 8 su iOS 27 (Xcode 27.1 con il runtime del simulatore iPhone Duo).
- Device Hub (Xcode > Open Developer Tool > Device Hub) per piegare, cerniera e ruotare il simulatore Duo.
- Un viewport che abilita le safe area (vedi sotto).
viewport-fit=cover e safe area CSS
Le variabili env delle safe area si applicano solo quando il viewport copre l'intero display:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Pad interattivo contenuto per i bordi. Fai sfumare gli sfondi fino ai margini per un effetto immersivo.
Scegli un unico owner dell'inset per tutta l'app. I due snippet qui sotto sono alternative, non una coppia da combinare.
Opzione A: shell per pagina
.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));
}
Salta l'Opzione B quando usi l'Opzione A.
Nelle scroll view, applica l'inset al contenuto, non solo il contenitore di scroll esterno, così gli sfondi possono scorrere a tutta larghezza mentre testo e pulsanti restano fuori dalla rail.
Usa quattro valori indipendenti. Non dare per scontato padding-left uguale padding-right. In RTL, la rail resta sul lato fotocamera; leggi entrambi i left e right valori env.
Due punti sfuggono facilmente:
- Uno scroller orizzontale dentro una pagina già con padding ha comunque bisogno del proprio padding finale. Altrimenti l'ultima card finisce sotto la rail.
- Un pulsante flottante impostato su
right: 16pxignora la rail. Aggiungi l'inset live a sinistra o a destra e ricalcolalo quando la finestra cambia dimensione.
Per le app Tailwind, @capgo/tailwind-capacitor espone pt-safe, px-safe, e utility correlate invece di ripetere env() ovunque.
Capacitor System Bars (safe area su Android e config su iOS)
Capacitor 8 introduce Barre di sistema nella @capacitor/core. Sostituisce il vecchio adjustMarginsForEdgeToEdge flag. Configuralo in capacitor.config.ts:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
plugins: {
SystemBars: {
insetsHandling: 'css',
},
},
};
export default config;
insetsHandling: 'css'(predefinito su Android): inietta--safe-area-inset-*variabili CSS quando i valori dell'ambiente WebView sono errati (build di Android WebView più vecchie).- L'opzione B (shell globale) usa il pattern di fallback presente nella documentazione System Bars. Salta l'opzione A se usi l'opzione 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));
}
A runtime puoi applicare stili o nascondere le barre con SystemBars.setStyle, SystemBars.show, e SystemBars.hide da @capacitor/core. There is no separate @capacitor/safe-area package separato: le safe area per le app Capacitor sono CSS env, variabili System Bars su Android, e i tuoi listener su resize / visualViewport.
@capacitor/status-bar
Il Plugin Status Bar controlla ancora overlay e stile su iOS:
import { StatusBar, Style } from '@capacitor/status-bar';
await StatusBar.setOverlaysWebView({ overlay: true });
await StatusBar.setStyle({ style: Style.Dark });
setOverlaysWebView({ overlay: true }) permette all'HTML di disegnare sotto la status bar. Usalo insieme al padding env, altrimenti taglierai i contenuti. Su iPhone Duo, la modalità overlay si applica comunque; la geometria di quale inset sia in alto e quale laterale deriva dalla safe area di sistema, non solo dalle API di Status Bar.
Non sovrapporre i margini automatici della WebView, il padding env a tutta pagina, e variabili inset della barra nativa senza una strategia esplicita. Questo schema causa spaziatura doppia su ogni iPhone, e peggiora quando la rail sposta gli inset sui lati.
contentInset del WebView iOS
In capacitor.config.ts, ios.contentInset corrisponde allo scroll view del WebView contentInsetAdjustmentBehavior. Molte app usano 'never' quando gestiscono gli inset in CSS. Se ti affidi all'aggiustamento automatico di UIKit e padding CSS env, scegli una sola fonte di verità.
Vedi Config iOS Capacitor.
Ridimensionamento WebView in apertura e chiusura
Capacitor mantiene una singola WebView a tutto schermo. Quando il Duo cambia posizione:
- UIKit aggiorna i safe area insets sul view controller.
- Il layout pass della WebView viene eseguito con nuove dimensioni.
- CSS
env(safe-area-inset-*)e100dvh/visualViewportsi aggiornano al prossimo style flush.
Ascolta i cambiamenti di geometria in JavaScript quando metti in cache posizioni basate sugli inset (bottoni floating, toast, overlay 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();
Il listener dello scroll copre i movimenti del visual-viewport che cambiano offsetLeft / offsetTop senza ridimensionare larghezza o altezza (es. spostamenti del viewport via tastiera su iOS).
Su Android, includi density nella android:configChanges nella tua main activity, così che i dispositivi pieghevoli e i cambi di dimensione dello schermo non distruggano il WebView. Capacitor 8 template lo documentano in guida all'aggiornamento a Capacitor 8.
Orientamento e classi di dimensione della finestra
Usa live larghezza per ramificare il layout, non screen.width catturata una sola volta all'avvio:
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';
In CSS, preferisci flex e grid con minmax() fisso 100vw larghezze. 100vw spesso ignora le barre di scorrimento e l'asimmetria della rail, causando overflow orizzontale.
Per la Split View sul display interno, ogni finestra può essere stretta (Apple cita una larghezza di circa 469pt). Testa la tua app agganciata a sinistra e a destra: la rail si attacca al bordo della finestra, quindi gli inset orizzontali possono cambiare lato tra le finestre.
Chrome HTML personalizzato vs barre native
La guida di Expo consiglia il nativo Stack e NativeTabs su iOS la gestione delle rail spetta al sistema. Lo stesso vale in Capacitor:
| Approccio | Comportamento di iPhone Duo |
|---|---|
| Header HTML fisso + tab inferiori | Rimane nei classici rettangoli di sicurezza in alto/in basso. Non si sposta nella rail. |
@capgo/capacitor-native-navigation con il sistema UITabBar / UINavigationBar |
Le barre seguono la posizione del sistema, inclusa la rail verticale. |
Se devi mantenere l'interfaccia HTML, controlla ogni schermata per sinistra e destra padding env, non solo in alto e in basso.

L'interfaccia HTML fissa non segue la rail: resta nei rettangoli sicuri superiore e inferiore. Le barre native, disegnate da UIKit o da @capgo/capacitor-native-navigation, si muovono con il sistema.
@capgo/capacitor-native-navigation su iPhone Duo
Native Navigation disegna barre vere UINavigationBar e UITabBar (o barre Material su Android) sopra una singola Capacitor WebView. Su iOS 27 con iPhone Duo, le tab bar di sistema si spostano nella rail laterale in modalità piegata e in vari orientamenti landscape. La curva del plugin la forma della tab bar resta sul bordo inferiore. L'azione centrale mantiene un alloggiamento circolare mentre la finestra si allarga.

A sinistra, la system tab bar è nella barra laterale. A destra, la tab bar curva resta in basso e l'azione centrale mantiene il suo alloggiamento.
Configura una sola volta
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(altezza nominale della tab bar quandotabbarHeightè0per le system bar ospitate)
Quando è Native Navigation a gestire le barre, applica il padding con le variabili del plugin qui sotto. Includono già l'overlap della barra nativa e i margini safe-area di UIKit quando una barra è attiva. Usa max() con fallback sulle env, così i margini restano corretti quando una variabile è 0 (ad esempio per le system tab bar ospitate) senza aggiungere padding del plugin e dell'env insieme.
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));
}
Non applicare anche il padding dell'Opzione A o B allo main stesso elemento.
Su iOS 26+ con una gestita dal sistema tab bar, tabbarHeight gli inset del plugin possono essere 0 così il contenuto web non viene spostato due volte. Usa --cap-native-system-tabbar-height quando serve spazio per le action fluttuanti sopra la rail.
safeAreaChanged
Iscriviti quando cambiano fold, lato rail o visibilità della bar:
await NativeNavigation.addListener('safeAreaChanged', ({ insets }) => {
console.log(insets.top, insets.right, insets.bottom, insets.left);
console.log(insets.navbarHeight, insets.tabbarHeight, insets.systemTabbarHeight);
});
Il plugin emette anche capNativeNavigation:safeAreaChanged su window con lo stesso insets payload.
Native code applica un piccolo gap superiore quando safeInsets.top è zero (status bar nella rail) così i pulsanti della navbar non toccano l'angolo arrotondato del display. Il padding web dovrebbe comunque usare env left/right per la rail.
setNavbar, setTabbar, e configure all return { insets } quando ti serve una lettura una tantum dopo aver aggiornato chrome.
Riferimento completo API: Documentazione Native Navigation.
Checklist di test
In Device Hub con il simulatore iPhone Duo:
- Piegato: i controlli non sono sotto la rail; i tap vengono registrati; gli inset orizzontali sono non nulli dove previsto.
- Aperto in verticale: le barre superiore/inferiore tornano dove iOS le posiziona; il layout non salta a un'architettura informativa diversa.
- Aperto in orizzontale / rail: il contenuto evita la rail laterale; le azioni fluttuanti usano
insets.rightoppureinsets.left, non hard-coded16px. - Piegatura parziale: nessuna UI critica nella zona della cerniera; modali e sheet restano utilizzabili.
- Split View: esegui l'app in finestre sinistra e destra; il bordo rail segue ciascuna finestra.
- Piegatura con una schermata aperta: stato, posizione di scroll e input dei form sopravvivono alla transizione.
- Simulatore iPhone classico: nessuna regressione.
Prompt per l'agente (copia e incolla)
Usa questo blocco con Claude Code, Cursor, Codex o un altro coding agent che lavora su 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.
Installa Skill Capgo in modo che gli agenti sappiano come configurare Native Navigation: npx skills add Cap-go/capgo-skills --skill capacitor-plugins.