返回知识中心
平台

VirtualKeyboard API:viewport 与 inset 边界

协调 geometrychange、visualViewport 与键盘 inset,让移动表单过渡期间的固定操作保持可达。

BotBrowser Team

文档中心

想直接进入 平台 文档吗?

这篇文章属于知识中心的指南库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

BotBrowser 可以在声明键盘策略、profile 和 viewport 的授权移动 fixture 中重复测试,但不能复现每一种原生键盘动画,也不能认证物理设备。更具体的问题是:可信的可编辑字段打开虚拟键盘时,哪一个表面改变、哪个 inset 可用、哪个操作仍然可达?本文把 VirtualKeyboard API 与通用 resize 处理,以及设备 profile 测试和viewport 测量指南中的选择分开说明。

要点

  • navigator.virtualKeyboard.overlaysContent 是页面选择的策略:键盘可以覆盖内容,而不是缩小 layout viewport。
  • geometrychange 报告当前键盘几何的 DOMRect;写入布局前应与 visualViewport 变化合并处理。
  • 只有浏览器和策略提供时,env(keyboard-inset-*) 才会暴露 CSS inset。保留 0px 的普通 padding 备用路径。
  • layout viewport、visual viewport 和键盘矩形回答不同问题,不能用一个 innerHeight 代替它们。
  • 将焦点、滚动、旋转、缩放和键盘关闭作为过渡测试;只截取打开状态并不完整。

目录

三个坐标表面与策略

layout viewport 是文档布局使用的坐标空间。visual viewport 是浏览器界面、捏合缩放或虚拟键盘占用空间后仍可见的部分。页面选择 overlay 行为时,VirtualKeyboard API 会增加键盘矩形。这些表面可以独立移动,因此单独的 window.innerHeight 不能描述聚焦表单的状态。

从 VirtualKeyboard geometrychange 经过键盘 inset 到可达固定操作的流程

表面回答的问题示例字段
layout viewport文档布局区域有多大?window.innerHeight
visual viewport用户此刻能看到什么?visualViewport.height
键盘矩形虚拟键盘覆盖哪些像素?virtualKeyboard.boundingRect

键盘让页面 resize 时,layout viewport 可能缩小,键盘矩形也可能为零或不在页面可用区域中。选择 overlay 模式时,layout viewport 可以保持稳定,而键盘覆盖底部边缘。Viewport resize 指南解释相关事件顺序;页面还需要明确的键盘策略与 CSS inset 契约。

不要在没有记录浏览器界面和缩放的情况下,仅用 innerHeight - visualViewport.height 推导键盘高度。这个差值只是估计,不是 API 保证。overlay 模式优先使用 virtualKeyboard.boundingRect,实际可见区域则记录 visualViewport.height、offsetTop 和 scale。

navigator.virtualKeyboard.overlaysContent = true 告诉支持该能力的浏览器由页面处理键盘覆盖区域。浏览器不会自动为每个固定页脚让路。页面必须使用几何值或 CSS environment variables,并保留可用的关闭和焦点路径。应在表单旅程打开键盘前设置策略,而不是每次 focus 都切换。

记录 API 是否存在、overlaysContent 赋值结果、活动 profile 标签和使用的 CSS 路径。桌面 profile 可能忽略该选项,这是能力边界,不是用户类别信号。

Fixture 与 inset 实现

下面的 fixture 固定提交操作,给表单提供滚动区域,并写入脱敏几何记录。记录不收集文本、账号标识符、原始按键事件或键盘内容。保持标记稳定,每次只改变矩阵中的一行。

<main class="screen">
  <form class="card" id="checkout">
    <label>Email <input name="email" type="email" autocomplete="email" /></label>
    <label>Address <input name="address" autocomplete="street-address" /></label>
    <p class="status" role="status" aria-live="polite"></p>
    <button class="submit" type="submit">Continue</button>
  </form>
</main>
<script>
  const vk = navigator.virtualKeyboard;
  if (vk) {
    vk.overlaysContent = true;
    vk.addEventListener('geometrychange', () => {
      const r = vk.boundingRect;
      document.documentElement.style.setProperty('--vk-bottom', `${r.height}px`);
      console.log(JSON.stringify({ type: 'keyboard', height: r.height, top: r.top }));
    });
  }
</script>
:root {
  --vk-bottom: 0px;
}
.screen {
  min-block-size: 100dvh;
  padding: 1rem;
}
.card {
  min-block-size: 140vh;
  padding-block-end: calc(5rem + var(--vk-bottom));
}
.submit {
  position: fixed;
  inset-inline: 1rem;
  inset-block-end: calc(1rem + var(--vk-bottom) + env(safe-area-inset-bottom, 0px));
}
@supports (padding: env(keyboard-inset-bottom)) {
  .card {
    padding-block-end: calc(5rem + env(keyboard-inset-bottom));
  }
  .submit {
    inset-block-end: calc(1rem + env(keyboard-inset-bottom) + env(safe-area-inset-bottom, 0px));
  }
}

JavaScript 自定义属性用于兼容只暴露几何值而不暴露 CSS environment variable 的引擎。@supports 只能证明语法可解析,不能证明 keyboard-inset-bottom 非零。焦点进入后和关闭后都检查计算出的 padding。键盘隐藏、overlay 关闭或浏览器不提供该功能时,0px 都是有效值。

keyboard-inset-top、keyboard-inset-right、keyboard-inset-bottom、keyboard-inset-left 以及宽高变体描述被遮挡的边缘。它们可用于为固定操作让出空间或为滚动容器预留空间,但不能替代 safe-area-inset-*。设备可能同时存在键盘和底部指示条安全区。用 max() 或 calc() 合并约束,并在 RTL 或竖排文字中优先使用逻辑属性。

如果关闭后备用路径保留了过多空间,在下一个几何事件中清除自定义属性,不要用负偏移隐藏操作。JavaScript 启动、导航恢复或 profile 不支持 navigator.virtualKeyboard 时,普通文档流仍应可用。

事件顺序与滚动所有者

geometrychange 可能接近 visualViewport.resize 和 visualViewport.scroll 到达。这些事件描述相关过渡,但没有保证的顺序。用一个调度器包住三个事件,每个动画帧只读取一次当前值:

let pending = false;
function scheduleLayout() {
  if (pending) return;
  pending = true;
  requestAnimationFrame(() => {
    pending = false;
    const vv = window.visualViewport;
    const k = navigator.virtualKeyboard?.boundingRect;
    document.documentElement.dataset.keyboard = k && k.height > 0 ? 'open' : 'closed';
    console.log(
      JSON.stringify({
        layoutHeight: window.innerHeight,
        visualHeight: vv?.height ?? null,
        visualOffsetTop: vv?.offsetTop ?? null,
        keyboardHeight: k?.height ?? 0,
      })
    );
  });
}
window.visualViewport?.addEventListener('resize', scheduleLayout);
window.visualViewport?.addEventListener('scroll', scheduleLayout);
navigator.virtualKeyboard?.addEventListener('geometrychange', scheduleLayout);

记录可以区分键盘过渡、浏览器界面和捏合缩放。keyboardHeight: 0 且 visualHeight 变小,并不能证明键盘关闭;也可能是工具栏或缩放变化。将 scale、offsetTop 和聚焦元素矩形一起保存。每帧一条记录可以避免竞争处理器写入过期 padding。

焦点落在可见区域下方的 input 时,浏览器可能滚动文档或内部 overflow: auto 容器。固定提交按钮可能保持可见而字段被遮住,也可能字段可见而按钮被键盘覆盖。滚动稳定后同时断言两个矩形。只选一个滚动所有者:等待焦点调整,只在字段底部超过可见键盘顶部时修正;不要每次 geometrychange 都调用 scrollIntoView()。

旋转和缩放是不同过渡。旋转可能在键盘事件前改变 layout viewport、visual viewport 和安全区 inset。捏合缩放以不同方式改变视觉坐标与 CSS 尺寸。把 scale、offsetTop 和安全区值放在独立矩阵行中。

记录验收结果

验收记录要指出失败的过渡,而不是只说键盘可见。保存行标识、聚焦控件、策略赋值结果、window.innerHeight、visualViewport.height、visualViewport.offsetTop、visualViewport.scale、键盘矩形、计算出的底部 padding 和最终操作矩形。

{
  "row": "D",
  "focused": "address",
  "overlaysContent": true,
  "layoutHeight": 844,
  "visualHeight": 512,
  "visualOffsetTop": 0,
  "keyboardTop": 512,
  "keyboardHeight": 332,
  "actionBottom": 496,
  "keyboardTopMinusActionBottom": 16,
  "actionReachable": true
}

如果 keyboardHeight 为正且 actionBottom 超过 keyboardTop,固定操作被覆盖。如果操作避开键盘但字段仍低于 keyboardTop,滚动所有者不正确。如果两个矩形都安全而 visualHeight 意外变小,应先检查浏览器界面或缩放,再修改 CSS。不要记录字段值、账号标识符、原始按键或设备序列数据。

诊断第一个分歧表面

如果没有 navigator.virtualKeyboard,将该行分类为不支持并保留普通 viewport 路径。如果属性存在但 overlaysContent 仍为 false,浏览器正在管理 resize;零矩形不能证明键盘关闭。如果 geometrychange 触发但计算出的 keyboard-inset-bottom 为零,使用 JavaScript 矩形路径并记录 CSS inset 不可用。如果矩形正确但操作被覆盖,检查 containing block 和安全区计算。如果操作避开 inset 而输入仍被隐藏,检查唯一滚动所有者和焦点时序。

让备用行为可测试

分别在缺少 navigator.virtualKeyboard、忽略 overlaysContent 赋值以及 CSS environment variables 解析但保持零值时渲染同一个表单。操作应通过普通文档流保持可达,用户也应能关闭键盘或改用其他输入路径。使用 feature flag 比较托管路径和显式路径,不改变表单标记;flag 只放在 fixture 记录中,不放入 fingerprinting 数据。

边界矩阵与 BotBrowser 能力

每一行使用一个新 profile 运行同一个 fixture。除非矩阵行明确改变,否则保持文本、字体加载、缩放和设备 scale factor 不变。有效失败信息要指出行和第一个分歧表面,例如“D 行内部滚动后操作矩形位于键盘顶部下方 18px”,而不是“移动键盘坏了”。

行变化预期观察含义
A关闭键盘策略普通 resize 或浏览器托管行为基线兼容性
B开启 overlay 并聚焦字段geometrychange、正 height、操作避开 inset显式键盘契约
C关闭键盘高度回到 0,没有旧 padding清理过渡
D内部滚动容器字段和操作都保持可达滚动所有权
E旋转或 200% 缩放viewport 与安全区字段独立变化非键盘过渡
F不支持或桌面 profileAPI 缺失或选项被忽略能力备用路径

保存 browserVersion、profile 标签、策略赋值、聚焦控件、layout 与 visual viewport 值、键盘矩形、安全区值和最终操作矩形。不要保存表单文本或原始键盘数据。每次打开后重复 C 行,发现过期的 --vk-bottom。

BotBrowser 能力边界

BotBrowser 支持匹配的移动 profile,profile 定义触控支持,并提供文档化的 --bot-mobile-keyboard 选项。在支持的 profile 中,可信地聚焦可编辑字段可能降低 visualViewport.height,同时保持 layout viewport 稳定,因此 fixture 可以重复键盘可见和触控检查。桌面 profile 可能忽略该选项。BotBrowser 不能替代物理设备、操作系统原生键盘及其动画,也不能覆盖每种应用布局,并且不保证每个浏览器提供非零键盘 inset。profile 选择可参考移动浏览器 profile 质量评审。

FAQ

overlaysContent 会调整 layout viewport 吗?

它用于让键盘覆盖内容,因此支持的浏览器可以保持 layout viewport 稳定;visual viewport 和键盘矩形仍会变化。应在每个浏览器中核对 innerHeight、visualViewport.height 和 boundingRect.height。

键盘 inset 与安全区 inset 相同吗?

不相同。键盘 inset 描述临时遮挡,安全区 inset 描述需要永久 padding 的设备或浏览器边缘。用 calc() 或 max() 合并它们,并测试关闭过程以清除旧 padding。

每次 geometrychange 都应调用 scrollIntoView() 吗?

不应。等待焦点滚动完成,在一个动画帧内检查控件和操作,仅在控件仍位于可见键盘边界后方时修正。一个滚动容器只保留一个所有者。

来源

实用结论

把 VirtualKeyboard 当作策略与几何契约。分别保存 layout、visual viewport、键盘矩形、安全区和滚动所有者;合并相关事件,并测试打开、关闭、旋转、缩放和不支持的 profile。BotBrowser 可以重复声明的移动条件并采集脱敏几何,但物理设备与原生键盘行为不属于这份证据。

#Virtual Keyboard#Viewport#Css Insets#Mobile Layout#Forms

本文属于指南: Web 平台能力面:一致性复核地图

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。