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.
Short Answer (for experienced devs)
Section titled “Short Answer (for experienced devs)”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.
- 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. - On the React side: Use the
VisualViewportAPI to detect keyboard height and manually adjust your layout if necessary, or useposition: stickywith atop: 0constraint, which is often more stable thanfixedin virtualized viewports.
The Deep Dive: Why it Happens
Section titled “The Deep Dive: Why it Happens”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+ ExampleScaffold( 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);}Solution B: React VisualViewport API
Section titled “Solution B: React VisualViewport API”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 environmentimport { 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 Componentconst 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> );};Prevention Checklist
Section titled “Prevention Checklist”- Avoid
height: 100vh: On mobile,100vhis unstable because it doesn’t account for the address bar or keyboard consistently. Use100%on thehtmlandbodytags instead. - Hardware Acceleration: Always apply
transform: translateZ(0)orwill-change: transformto fixed headers. This promotes the element to its own compositor layer, reducing redraw flicker. - Input Focus Management: If using
flutter_inappwebview, check theshouldOverrideUrlLoadingoronLoadStopevents to ensure no scripts are conflicting with focus events.
Related Concerns & Follow-up
Section titled “Related Concerns & Follow-up”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.