移动端软键盘弹出时,position: fixed 元素之所以会被“顶起”或出现错位,本质原因在于 fixed 元素锚定的是视觉视口,而软键盘会压缩该视口却不会立即触发页面重排;再加上 resize 事件存在延迟甚至不触发,以及 env(keyboard-inset-bottom) 兼容性有限,想要稳定解决问题,通常需要结合 visualViewport 监听和 fallback 兜底方案。

并不是你的 position: fixed 写法有问题,而是浏览器会把 fixed 元素定位到「视觉视口(visual viewport)」上。当移动端软键盘弹出时,视觉视口高度会被压缩,但 fixed 元素的定位计算值往往不会同步重算,因此底部按钮、输入框工具栏这类固定定位元素就容易悬浮在键盘上方,形成明显空白和定位偏移。
fixed 锚定的是 visual viewport,不是 layout viewport
在 iPhone Safari 以及多数安卓 WebView 场景中,软键盘通常会被浏览器当成一层“覆盖层”处理:它只会压缩 visual viewport.height,却不会触发 layout viewport 的重新排版。也正因为如此,像 bottom: 0、100vh,甚至 @media (max-height: ...) 这类常见写法,依赖的其实是初始视口或 layout 视口尺寸,自然无法准确感知软键盘弹出后 visual viewport 被动态挤压的变化。
window.innerHeight在键盘弹出后虽然会变小,但 fixed 元素的定位坐标很多时候仍按旧值渲染,因此会出现底部 fixed 布局错位document.documentElement.clientHeight通常保持不变,它反映的是 layout viewport,高度信息与用户真实看到的可视区域并不一致- 即使加上
viewport-fit=cover,它影响的也只是刘海屏安全区,并不会改变 fixed 元素依附视觉视口的定位逻辑
resize 事件不可靠,focusin 又太早
很多人会第一时间想到监听 resize 来判断软键盘是否弹出,但在移动端浏览器里,这种方案并不稳定,实际表现往往是:
- 在 iOS Safari 中,
resize往往存在明显延迟,通常要等键盘完全展开后 300–500ms 才触发,而且还可能连续触发多次 - 部分安卓 WebView 对
resize的支持并不一致,有的根本不会触发,有的只会在软键盘收起时补发一次 focusin触发时机又偏早,此时键盘尚未真正弹出,getBoundingClientRect()读取到的还是旧坐标,直接用于修正位置就很容易产生偏差
env(keyboard-inset-bottom) 不是万能解,仅限 iOS 16.4+
从 CSS 角度看,env(keyboard-inset-bottom) 的确是一个相对优雅的处理方式,但它并不是跨平台通用方案。目前它只在 iOS Safari 16.4+ 中可用,安卓浏览器整体不支持,旧版 iOS 还可能直接忽略整条规则,甚至带来 CSS 解析问题。
- 必须使用
@supports (bottom: env(keyboard-inset-bottom))做能力检测包裹,否则降级逻辑容易失效 - 不能和
svh这类单位随意混用,例如bottom: calc(0px + 100svh)就属于无效写法 - 即使在支持环境下启用,iOS 在软键盘收起后
window.innerHeight也不会立刻恢复,仍需要通过阈值判断并手动清理状态
归根结底,想解决“CSS 固定定位被移动端软键盘顶起”的问题,真正可靠且兼容性更好的思路一定是采用“组合方案”:优先使用 visualViewport.height 作为主监听方式,覆盖现代移动浏览器;不支持时再回退到 focusin/blur + setTimeout(() => {}, 0),并根据场景动态切换到 position: absolute;最后再结合 padding-bottom 或 margin-bottom 做额外兜底。只改某一个 CSS 属性,或者只依赖单一事件监听,看似简单,但在复杂机型、不同 WebView 和真实用户环境下,往往很难稳定生效。
