结论是别死守position: fixed,iOS键盘弹起时它不可靠——因Safari将fixed锚定在视觉视口底部,而键盘仅推页面却不重算定位;须组合使用env(keyboard-inset-bottom)、focusin/blur+absolute模拟及visualViewport监听。

直接说结论:别死守 position: fixed,iOS 键盘弹起时它就是不可靠的——不是你写错了,是 Safari 把 fixed 锚定在视觉视口(visual viewport)底部,而键盘只推页面、不重算定位。必须用组合策略:优先用 env(keyboard-inset-bottom),fallback 到 focusin/blur + absolute 模拟,再兜底监听 visualViewport。
为什么 env(keyboard-inset-bottom) 是首选但不能全靠它
iOS Safari 16.4+ 原生支持这个环境变量,键盘弹出时它自动变成当前键盘高度(单位 px),收起后回退为 0。写法简单:bottom: env(keyboard-inset-bottom, 0px)。但它有硬限制:
- 安卓完全不识别,旧版 Safari(如 15.3)也忽略整条规则
- 必须搭配
<meta name="viewport" content="viewport-fit=cover">,否则不生效 - 必须写 fallback 值(
0px),不能只写env(keyboard-inset-bottom),否则键盘收起后按钮悬空 - 不能和
env(safe-area-inset-bottom)混用——后者管刘海,不管键盘
用 focusin/blur + absolute 模拟 fixed 的实操要点
这是目前兼容性最广的 JS 方案,但细节极多,错一个就失效:
- 监听
focusin(不是focus),目标必须包括input、textarea、[contenteditable],漏掉textarea就等于漏掉一半场景 -
focusin回调里立刻检查document.activeElement === inputEl,防止异步焦点转移导致状态错位 - 必须用
setTimeout(() => {}, 0)延迟读取getBoundingClientRect(),否则坐标仍是键盘弹出前的旧值 - 计算
bottom用window.innerHeight - inputRect.bottom,不是写死数值,适配不同机型键盘高度 -
blur后必须立刻清除内联样式:el.style.removeProperty('top')和el.style.removeProperty('bottom'),再恢复position: fixed和bottom: 0
为什么 visualViewport API 更准但得降级兜底
它直接暴露用户真正看到的可视区域高度(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 以下、部分安卓 WebView 不支持,必须 fallback 到
focusin/blur 方案 - 键盘收起后
window.innerHeight常延迟几百毫秒才恢复,不能依赖它做即时判断
最容易被忽略的底层陷阱
所有方案都卡在这几个地方,且无任何报错提示:
-
getBoundingClientRect()在页面用了transform、overflow: hidden或position: fixed父容器时,返回坐标严重失真 - 监听
window.onresize在 iOS Safari(尤其是 15.4+)中触发率低于 20%,且常只在键盘收起时才触发一次,完全漏掉弹出瞬间 - 所有坐标计算前,必须确认
document.activeElement === inputEl,否则状态错位 -
requestAnimationFrame或setTimeout的时机比你想的更关键——早 1ms 读不到新高度,晚 10ms 页面已滚动错位


















