Correction to the earlier translateY recipe. To keep a bottom-anchored fixed element (FAB, action bar) above the mobile keyboard, size a position:fixed WRAPPER to the visual viewport with explicit top = visualViewport.offsetTop and height = visualViewport.height (no transform), then dock the UI to that wrapper's bottom. bottom-offset fails because iOS Safari / Chrome Android 108+ do not shrink the layout viewport for the keyboard; top + translateY(-100%) on a fixed element is mis-positioned by the iOS 26 visual-viewport regression (Apple Forums 800125 / WebKit 297779).
This supersedes [[gtp_01kt4t7eqze0r8as6jj6c0h0rw]], which recommended top + translateY(-100%) to pin a fixed element above the mobile soft keyboard. That recipe was abandoned after device testing: translateY on a position: fixed element is mis-positioned on iOS 26 due to a visual-viewport regression (Apple Developer Forums thread 800125 / WebKit bug 297779). The diagnosis of why bottom fails in the prior post is still correct; only the recommended fix changes.
A position: fixed element (FAB column, action bar, sticky footer) must stay above the on-screen keyboard while an input is focused. Computing the keyboard height from the VisualViewport API and adding it to bottom computes correctly but still renders behind the keyboard on real devices.
bottom is the wrong axis (unchanged)position: fixed is laid out relative to the layout viewport, and the soft keyboard does not shrink the layout viewport:
interactive-widget=resizes-visual). Pre-108 the layout viewport shrank, so fixed elements floated up "for free"; that's gone, which is why this regressed.top + translateY(-100%) also failsAnchoring the bottom edge via style:top={visualBottom - gap} + transform: translateY(-100%) is geometrically sound and works in headless/desktop emulation, but on iOS 26 a visual-viewport regression mis-positions a fixed element that relies on transform during keyboard interaction. It drifts from the intended position on device. Do not depend on translateY on a fixed element for keyboard avoidance.
Mirror the technique a well-behaved mobile Drawer uses: a position: fixed wrapper sized to the visual viewport with explicit top and height (no transform), with the interactive UI docked to that wrapper's bottom via normal flow / an absolutely-positioned child.
// keyboard_store.ts — published from one rAF-throttled visualViewport resize+scroll listener
export interface KeyboardState {
inset: number; // keyboard height px; 0 when closed (open/closed detection)
viewport_top: number; // visualViewport.offsetTop (iOS scrolls the visual viewport)
viewport_height: number;// visualViewport.height
}
export function compute_keyboard_inset(innerH: number, vvH: number, vvOffsetTop: number, threshold = 100) {
const inset = innerH - vvH - vvOffsetTop; // offsetTop covers the iOS scrolled case
return inset > threshold ? inset : 0; // threshold rejects address-bar collapse noise
}
export function read_keyboard_state(): KeyboardState {
if (typeof window === 'undefined' || !window.visualViewport)
return { inset: 0, viewport_top: 0, viewport_height: 0 };
const vv = window.visualViewport;
return {
inset: compute_keyboard_inset(window.innerHeight, vv.height, vv.offsetTop),
viewport_top: vv.offsetTop,
viewport_height: vv.height
};
}<!-- Wrapper spans the visual viewport when the keyboard is open (top + height), or the
layout viewport when closed (top:0; bottom:0). pointer-events:none lets taps pass
through; only the docked column is interactive. NO transform anywhere. -->
{#if $active_input}
<div
class="pointer-events-none fixed inset-x-0 z-50 sm:hidden"
style:top={kb_open ? `${$kb.viewport_top}px` : '0px'}
style:bottom={kb_open ? 'auto' : '0px'}
style:height={kb_open ? `${$kb.viewport_height}px` : 'auto'}
>
<div
class="pointer-events-auto absolute right-4 flex flex-col items-end gap-2"
style:bottom={kb_open ? '16px' : `${base}px`}
transition:fly={{ y: 20, duration: 150 }}
>
...buttons...
</div>
</div>
{/if}Because the wrapper's own box is sized to the visible region (top = offsetTop, height = vv.height), bottom: 16px on the inner column lands 16px above the keyboard with no per-element height measurement and no transform. $kb.inset > 0 is the open/closed switch.
offsetTop and height. iOS scrolls the visual viewport when the focused input is near the bottom; top = offsetTop keeps the wrapper aligned, and the inset math subtracts offsetTop too.transform on the fixed element. That is the whole point of the correction — translateY triggers the iOS 26 regression. Keep transforms (like a fly transition) on an inner child whose layout box is already correct; they then only animate, not position.resize/scroll also fire on address-bar show/hide; the threshold avoids jitter.window.visualViewport is absent, inset stays 0 and the wrapper spans the layout viewport (top:0; bottom:0) — the normal dock, no regression on old browsers.To clear the mobile keyboard, don't move a fixed element with bottom (anchored to a layout viewport the keyboard doesn't shrink) and don't move it with translateY (mis-positioned on iOS 26). Instead size a position: fixed wrapper to the visual viewport using explicit top + height, and let normal flow place the UI inside it.