VisualViewport 是 iOS 和部分 Android 浏览器中感知虚拟键盘状态的唯一可靠方案,需监听 visualViewport.resize 事件并基于 height 差值动态调整 fixed 元素位置,避免重排与冲突。

VirtualKeyboard API 不可用时,VisualViewport 是唯一可靠选择
在 iOS 和部分 Android 浏览器中,keyboard 事件和 VirtualKeyboard API 要么未实现(如 Safari),要么行为不一致(如 Chrome on Android 需要显式启用 flag)。此时只能依赖 visualViewport 的尺寸变化来感知虚拟键盘状态——它在键盘弹出/收起时会触发 resize 事件,并更新 height 和 scale 属性。
注意:不能监听 window.resize,它在键盘弹出时通常不触发(尤其 iOS);必须用 visualViewport.addEventListener('resize', ...)。
-
visualViewport在 iOS 13.4+、Chrome 61+、Edge 79+ 可用,但 Safari 中offsetTop始终为 0,需靠height缩减量判断 - 键盘弹出时,
visualViewport.height明显变小(比如从 667px → 400px),而layoutViewport.height不变 - 不要依赖
scrollY或pageYOffset,它们在键盘唤起时可能滞后或归零
用 height 差值判断键盘是否打开,而非绝对阈值
不同设备、不同输入法、甚至同一设备横竖屏切换都会导致键盘高度不同。硬编码 “小于 500px 就是键盘打开了” 会误判——比如小屏手机正常浏览时 visualViewport.height 本就只有 480px。
正确做法是记录键盘关闭时的基准高度,在每次 resize 时计算差值:
let baselineHeight = visualViewport.height;
visualViewport.addEventListener('resize', () => {
const delta = baselineHeight - visualViewport.height;
if (delta > 150) {
// 键盘大概率已弹出
adjustFixedElement();
} else if (delta < 50) {
// 恢复原始位置(留 50px 容差,避免抖动)
resetFixedElement();
}
});
// 页面加载后主动设一次 baseline(避开初始缩放/导航栏影响)
setTimeout(() => {
baselineHeight = visualViewport.height;
}, 300);
- 容差值(150px / 50px)不是 magic number:iOS 键盘最小高度约 216px(数字键盘),Android 通常 ≥250px,取 150 是为覆盖半屏软键盘等边缘情况
- 必须加
setTimeout设 baseline:页面刚加载时visualViewport.height可能包含地址栏高度,等渲染稳定后再采样 - 不要在
resize里直接调用scrollIntoView或修改top—— 会与浏览器原生滚动冲突,优先用transform: translateY()
fixed 元素需用 transform 而非 top 动态避让,否则触发重排
用 top 或 bottom 修改 position: fixed 元素会强制同步布局(reflow),在键盘频繁收放时造成卡顿。更优解是仅操作 transform: translateY(),它走合成层,不触发重排。
假设你有一个底部固定按钮 <div id="submit-btn"></div>,需上移避开键盘:
function adjustFixedElement() {
const btn = document.getElementById('submit-btn');
const delta = baselineHeight - visualViewport.height;
// 上移距离 = 键盘遮挡高度 - 元素自身高度余量
const shift = Math.max(0, delta - btn.offsetHeight * 0.3);
btn.style.transform = `translateY(-${shift}px)`;
}
function resetFixedElement() {
document.getElementById('submit-btn').style.transform = 'translateY(0)';
}
- 减去
btn.offsetHeight * 0.3是为了保留一点“可点击余量”,避免按钮被键盘顶到刚好贴边,用户点不到 - 务必在
reset时显式设回translateY(0),不要用''清空——残留的内联transform会导致下次调整失效 - 如果元素有
transition: transform,需在键盘收起时临时移除,否则动画延迟会让它“慢半拍掉下来”
兼容 fallback:检测 visualViewport 是否存在并降级处理
旧版 Safari(visualViewport 为 undefined。此时无法精确感知键盘,只能做弱化处理:监听 focus 事件 + 强制滚动到元素下方。
if ('visualViewport' in window) {
// 启用 visualViewport 方案
} else {
// 降级:对所有 input/textarea 绑定 focus
document.querySelectorAll('input, textarea').forEach(el => {
el.addEventListener('focus', () => {
setTimeout(() => {
const rect = el.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom > viewportHeight * 0.7) {
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 100);
});
});
}
- 这个 fallback 不解决定位偏移问题,只缓解“被键盘盖住”的最差体验
-
setTimeout是因为 focus 后键盘动画尚未完成,立即getBoundingClientRect可能拿不到真实位置 - 不要尝试用
matchMedia('(max-height: 400px)')这类媒体查询模拟,它无法响应键盘动态变化
真正棘手的是 iOS 上 visualViewport 的 offsetTop 始终为 0,且键盘收起时 resize 事件可能比实际视觉恢复慢一帧——这意味着你得在 resize 后加 requestAnimationFrame 再读取尺寸,否则可能多移了一点。

















