你以为工具栏的键盘导航只能靠 JS 写几百行?今天 focusgroup 一个属性彻底原生化了

配了三年工具栏,每次加个按钮都要回头改 roving tabindex——今天一个 HTML 属性把这件事彻底原生化了


做工具栏、选项卡、菜单这些复合组件,最麻烦的不是 UI,是键盘导航。

ARIA 规范要求这类组件提供「一个 Tab 停驻点,再用方向键在项目间移动」,这个模式叫「roving tabindex」。听起来简单,手写的话要处理这些:

  • 每个非当前项设置 tabindex="-1",当前项设 tabindex="0"
  • 监听左右方向键,移出范围要跳到另一端(或不跳)
  • 处理 RTL 模式:方向键方向要反过来
  • 记住用户上次停在哪个,按 Tab 出去再回来要还原
  • 跳过已禁用/隐藏的项
  • 动态增删项目时更新索引

React、Angular CDK、Fluent UI 各自实现了一套,本质是同一坨代码。

Chrome 150 把这件事做成了浏览器原生属性:focusgroup

三行代码,替代几百行 JS

<!-- 之前:手动 roving tabindex -->
<div role="toolbar" onkeydown="handleToolbarNav(event)">
  <button tabindex="-1">Bold</button>
  <button tabindex="0">Italic</button> <!-- 当前项 tabindex=0 -->
  <button tabindex="-1">Underline</button>
</div>

<!-- 之后:focusgroup 一个属性 -->
<div focusgroup="toolbar wrap">
  <button>Bold</button>
  <button>Italic</button>
  <button>Underline</button>
</div>

浏览器自动接管:方向键导航、单一 Tab 停驻点、末项记忆。开发者只留「选中状态切换、菜单展开、自定义指令」这些业务逻辑。

六种模式,覆盖常见场景

focusgroup 第一个 token 声明组件类型,后面的 token 调行为:

Token 组件类型 默认方向 默认环绕
toolbar 工具栏 inline(左右) 不环绕
tablist 选项卡 inline 环绕
radiogroup 单选组 inline 不环绕
listbox 列表框 block(上下) 不环绕
menu 菜单 block 环绕
menubar 菜单栏 inline 环绕

修饰符:inline/block 限定轴,wrap 允许跳到头,nomemory 关闭焦点记忆。

初始焦点可以用 focusgroupstart 指定:

<div focusgroup="toolbar wrap">
  <button focusgroupstart>Bold</button>
  <button>Italic</button>
  <button>Underline</button>
</div>

嵌套 focusgroup也没问题,子组自动退出父组的箭头导航,自己管自己。

三个坑

  1. Safari/Firefox 暂不支持。Chrome/Edge 150+ 可用,生产环境建议 polyfill:@microsoft/focusgroup-polyfill,约 400 周下载量
  2. Home/End 键行为有边界情况。当焦点组在可滚动区域内,Home/End 会优先处理滚动,需要 polyfill 兜底(已在修复中,2026-07 后已修)
  3. 已有的 roving tabindex JS 要删掉。混用会导致双重处理,迁移时要把旧的 onkeydown 监听器移除

三步下一步

  1. 在 Chrome/Edge 150+ 打开 about://flags → Experimental Web Platform features,实战体验官方 demos
  2. 审计项目里用了 roving tabindex 的组件(工具栏、Tab 栏、选项卡组),列出来准备迁移
  3. 先挑一个最简单的组件,用 polyfill 试跑,验证键盘行为和屏幕阅读器播报无误后再上生产

评论区

0 条评论

登录后可评论。

铁锈·Rust工具链 380 阅读