移动端 fixed 元素被锚定在视觉视口,软键盘弹出时视口变化但 fixed 不重算,导致遮挡;window.innerheight 和 100vh 静态不响应键盘变化;env(keyboard-inset-bottom) 仅 ios 16.4+ 支持且需降级;focusin 比 resize 更可靠,需监听所有可聚焦元素并延时读 dom;visualviewport.height 是最准动态高度源,但需兼容老版本及处理 safari 键盘收起延迟等细节。

这不是 CSS 写错了,而是 position: fixed 在移动端被锚定在「视觉视口(visual viewport)」上,而软键盘弹出时,这个视口高度被压缩(Android)或上推冻结(iOS),但浏览器不会重算 fixed 元素的位置——结果就是底部按钮飘到屏幕中间、输入框被遮住,甚至整个元素被顶出可视区。
为什么 window.innerHeight 和 100vh 都失效
因为这些值是静态基准,不随键盘动态更新:
-
window.innerHeight在 Android 上会缩窄,但在 iOS Safari(尤其 15.4+)中几乎不变,导致跨平台行为割裂 -
100vh是初始布局视口高度,键盘弹出后它不会自动变成100dvh或响应变化 -
bottom: 0的计算始终基于原始视口,不是用户当前真正看到的区域
env(keyboard-inset-bottom) 能用但不能全靠它
这是 iOS Safari 16.4+ 原生支持的最优解,但它有硬性限制:
- 安卓 WebView 完全不识别该变量,写了等于没写
- 必须配合
@supports (bottom: env(keyboard-inset-bottom))降级,否则旧版 Safari 会忽略整条规则 - fallback 必须写成
bottom: env(keyboard-inset-bottom, 0px),只写env(keyboard-inset-bottom)会导致键盘收起后按钮悬空 - 需要
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">启用支持
监听 focusin 比 resize 更可靠
window.addEventListener('resize') 在 iOS 上触发率低于 20%,且常只在键盘收起时才触发一次,漏掉弹出瞬间;focusin 才是唯一稳定入口点:
- 必须监听所有可聚焦元素:
input、textarea、[contenteditable],漏掉textarea就等于漏掉一半场景 - 要用
focusin,不是focus——前者冒泡更可靠,能捕获深层子元素的聚焦 - 回调里立刻检查
document.activeElement === inputEl,防止异步焦点转移导致状态错位 - 别在
focusin回调里直接读getBoundingClientRect(),得加setTimeout(() => {}, 0)等 DOM 更新完成
用 visualViewport.height 是现代浏览器首选
它返回的是用户当前真正看到的高度,比抖动的 resize 和不稳定的 window.innerHeight 更准:
- 页面加载时缓存基准值:
const baseHeight = visualViewport.height - 监听
visualViewport.addEventListener('resize', () => { ... }),计算差值:const kbHeight = baseHeight - visualViewport.height - 用
transform: translateY(${kbHeight}px)调整按钮位置,比改bottom更少触发重排 - iOS Safari 15.4+、Chrome 61+ 支持;老版本必须降级,不能只靠它
真正难的不是选哪个方案,而是处理 iOS 键盘收起后 window.innerHeight 不立即恢复、focusin 触发早于键盘完全展开、以及 Safari 对 window.scrollTo() 的节流策略——这些细节不手动处理,任何方案都会在真实机型上掉链子。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











