Skip to content

Fix HTML Fixed Header Jumping in Flutter InAppWebView (iOS)

A developer in a React-focused mobile community recently asked why their position: fixed header was “teleporting” or juddering whenever a text input was focused inside a flutter_inappwebview on iOS. Despite setting resizeToAvoidBottomInset to false in their Flutter Scaffold, the header would still glitch or disappear entirely as the keyboard animated in.

This is a notorious cross-platform issue involving the discrepancy between how the iOS WKWebView handles the “Visual Viewport” and how Flutter’s Scaffold manages layout constraints.

The “judder” is caused by a race condition between the Flutter Scaffold resizing the native view and the iOS WKWebView attempting to scroll the focused input into view.

  1. On the Flutter side: Set resizeToAvoidBottomInset: false. This prevents the entire WebView widget from physically shrinking, which is usually what causes the fixed elements to recalculate their position mid-animation.
  2. On the React side: Use the VisualViewport API to detect keyboard height and manually adjust your layout if necessary, or use position: sticky with a top: 0 constraint, which is often more stable than fixed in virtualized viewports.

On iOS, when the software keyboard appears, the WKWebView (which powers flutter_inappwebview) doesn’t necessarily “shrink” the HTML height. Instead, it shifts the “Visual Viewport” (the area currently visible to the user) while the “Layout Viewport” (the actual DOM area) remains the same size.

If your Flutter Scaffold has resizeToAvoidBottomInset: true, Flutter shrinks the native view container. This forces the WebView to trigger a DOM reflow because the 100vh or 100% height just changed. The combination of iOS’s internal scrolling logic and the DOM reflow results in that “juddering” motion as the header tries to stick to a moving target.

Solution A: Disable Flutter Resizing and Use CSS env()

Section titled “Solution A: Disable Flutter Resizing and Use CSS env()”

Applicable to Flutter 3.x and React 18

By setting resizeToAvoidBottomInset: false, you tell Flutter to let the keyboard overlay the app. The WebView remains at full height, preventing the DOM reflow. You then handle the “safe area” via CSS.

1. Flutter Configuration

// Flutter 3.16+ Example
Scaffold(
resizeToAvoidBottomInset: false, // Prevents the WebView widget from shrinking
body: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri("https://your-app.com")),
initialSettings: InAppWebViewSettings(
// Ensure the webview doesn't try to handle safe areas aggressively
allowsInlineMediaPlayback: true,
contentMode: InAppWebViewContentMode.MOBILE,
),
),
);

2. React/CSS Implementation To ensure your input fields aren’t hidden behind the keyboard, use the scroll-margin property or ensure your container has enough bottom padding.

/* CSS 3 - Illustrative example */
.header-fixed {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1000;
/* Use transform: translateZ(0) to force GPU layer for smoother movement */
transform: translateZ(0);
}
.content-container {
/* Ensure padding for the bottom so inputs can be scrolled above the keyboard */
padding-bottom: env(safe-area-inset-bottom);
}

Applicable to React 18, Node 20 (Build environment)

If you must keep resizeToAvoidBottomInset: true (perhaps because other Flutter UI elements need to move), you should use the VisualViewport API in your React code to detect the keyboard and manually offset the header.

// React 18 Hook - illustrative example — verify in your environment
import { useEffect, useState } from 'react';
export function useKeyboardOffset() {
const [offset, setOffset] = useState(0);
useEffect(() => {
if (!window.visualViewport) return;
const handleResize = () => {
// The difference between the window height and the visual viewport
// height is roughly the height of the keyboard.
const keyboardHeight = window.innerHeight - window.visualViewport.height;
setOffset(keyboardHeight > 0 ? keyboardHeight : 0);
};
window.visualViewport.addEventListener('resize', handleResize);
return () => window.visualViewport.removeEventListener('resize', handleResize);
}, []);
return offset;
}
// Usage in Component
const Header = () => {
const keyboardOffset = useKeyboardOffset();
return (
<header style={{
position: 'fixed',
top: 0,
// On some iOS versions, fixed elements behave better if we
// explicitly reset their top position during viewport shifts.
transform: `translateY(${window.visualViewport.offsetTop}px)`
}}>
My Header
</header>
);
};
  1. Avoid height: 100vh: On mobile, 100vh is unstable because it doesn’t account for the address bar or keyboard consistently. Use 100% on the html and body tags instead.
  2. Hardware Acceleration: Always apply transform: translateZ(0) or will-change: transform to fixed headers. This promotes the element to its own compositor layer, reducing redraw flicker.
  3. Input Focus Management: If using flutter_inappwebview, check the shouldOverrideUrlLoading or onLoadStop events to ensure no scripts are conflicting with focus events.

Is this behavior the same on Android? No. Android typically handles the resizeToAvoidBottomInset much more cleanly. The browser engine (Chromium) updates the layout viewport immediately, and position: fixed elements rarely “jump” because they anchor to the layout viewport, not the visual viewport.

Does keyboardResizeMode in InAppWebViewSettings help? Yes. In recent versions of flutter_inappwebview, you can try InAppWebViewSettings(keyboardResizeMode: WindowProxyKeyboardResizeMode.RESIZE). This attempts to bridge the Flutter/Native keyboard logic more effectively, but resizeToAvoidBottomInset: false remains the most reliable “manual” fix.

What about position: sticky? In many cases, replacing position: fixed with position: sticky (and top: 0) inside a scrollable container solves the juddering. Sticky elements anchor to their nearest scrollable parent, which is often less sensitive to Visual Viewport changes than the global fixed coordinate system.