position: fixed在移动端被顶出屏幕是因为其锚定视觉视口(visual viewport),而软键盘弹出会压缩或上推该视口但不触发位置重算;Android缩window.innerHeight,iOS冻结layout viewport并上推页面,导致fixed元素基于旧快照渲染而飘浮或移出可视区。

这不是你 CSS 写错了,position: fixed 在移动端被顶出屏幕,是因为它锚定的是「视觉视口(visual viewport)」,而软键盘弹出会压缩或上推这个视口——但浏览器不会重新计算 fixed 元素的位置。
visual viewport 被压缩,但 fixed 不重排
Android 大多直接缩小 window.innerHeight,iOS 则倾向于冻结 layout viewport、上推页面内容,同时只改变 visual viewport 的高度。无论哪种行为,fixed 元素的渲染坐标都基于键盘弹出前的视口快照,结果就是按钮浮在键盘上方、甚至完全移出可视区域。
常见错误认知包括:
- 以为
bottom: 0或100vh能自适应——它们不随 visual viewport 动态变化 - 依赖
window.addEventListener('resize', ...)——iOS 上触发延迟严重,安卓部分 WebView 根本不触发 - 用
document.documentElement.clientHeight判断——它反映的是 layout viewport,和用户实际看到的区域脱节
env(keyboard-inset-bottom) 为什么不能单独用
这个 CSS 环境变量确实能返回键盘高度,但它只在 iOS Safari 16.4+ 生效,安卓全系不支持,且必须满足两个硬条件:
立即学习“前端免费学习笔记(深入)”;
-
<meta name="viewport" content="viewport-fit=cover">必须存在,否则变量值恒为0px - 必须用
@supports (bottom: env(keyboard-inset-bottom))包裹,否则旧版 Safari 可能忽略整条规则甚至解析失败 - 不能和
svh混用,例如bottom: calc(0px + 100svh)是无效写法
focusin 时机太早,getBoundingClientRect() 拿到的是旧坐标
监听 focusin 确实是必要动作,但它的触发点远早于键盘完全展开。此时调用 input.getBoundingClientRect().bottom 返回的仍是键盘未弹出时的位置。
安全做法是:
- 在
focusin回调中加setTimeout(() => { ... }, 0),确保 DOM 重排完成后再读取 - 用
window.innerHeight - inputRect.bottom计算 bottom 偏移,而不是写死数值 - 多个输入框共用同一底部栏时,需全局记录
document.activeElement,避免重复切换或漏恢复 -
blur后立刻清除 inline 样式:el.style.removeProperty('top')和el.style.removeProperty('bottom')
真正稳定的方案永远是组合
单一手段必穿帮。现代浏览器优先用 visualViewport.height 监听真实可视高度;降级到 focusin/blur + setTimeout 动态切 position: absolute;再用 padding-bottom 或 margin-bottom 做兜底。iOS 键盘收起后 window.innerHeight 不立即恢复、Safari 对 window.scrollTo() 有节流策略——这些不是边缘情况,是所有方案都必须处理的底层事实。


















