I’m building a React + Capacitor app with a sticky header (position: sticky; top: 0;).
On iOS (Safari PWA and Capacitor WebView), when the keyboard opens, the header suddenly stretches down as if safe-area-inset-top increased. When the keyboard closes, the header goes back to normal.
I want the header to remain stable: no stretching down when keyboard opens, but also no ugly black status bar (so I prefer StatusBar.setOverlaysWebView({ overlay: true })).
Example Code
CSS:
:root {
--header-height: 56px;
--safe-top: env(safe-area-inset-top);
--header-total: calc(var(--header-height) + var(--safe-top));
}
.app-header {
position: sticky;
top: 0;
z-index: 40;
height: var(--header-total);
padding-top: var(--safe-top);
background: white;
border-bottom: 1px solid #ddd;
}
React Header:
export default function Header() {
return (
<header className="app-header">
<div style={{ height: 'var(--header-height)' }}>
<div className="h-full flex items-center justify-between">
<button>☰</button>
<div>LoyalFlow</div>
<button>⚙</button>
</div>
</div>
</header>
);
}
Capacitor init:
import { Capacitor } from '@capacitor/core';
async function initNativeUi() {
if (!Capacitor.isNativePlatform()) return;
const { StatusBar, Style } = await import('@capacitor/status-bar');
await StatusBar.setOverlaysWebView({ overlay: true }); // ✅ no black bar
await StatusBar.setStyle({ style: Style.Dark });
await StatusBar.show();
}
What I’ve tried:
Locking header height with min-height and max-height.
Using
translateZ(0)and will-change.Using visualViewport resize events with a small "poke" (
translateY -1pxand back).Switching
overlay: falsefixes the stretching — but introduces the ugly black bar at the top, which I want to avoid.
How can I prevent the sticky header from stretching down on iOS when the keyboard opens, while keeping StatusBar.setOverlaysWebView({ overlay: true }) (no black bar)?
Is there a reliable pattern to "lock" the safe-area inset on iOS so the header doesn’t move with the keyboard?