你以为工具栏的键盘导航只能靠 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也没问题,子组自动退出父组的箭头导航,自己管自己。
三个坑
- Safari/Firefox 暂不支持。Chrome/Edge 150+ 可用,生产环境建议 polyfill:
@microsoft/focusgroup-polyfill,约 400 周下载量 - Home/End 键行为有边界情况。当焦点组在可滚动区域内,Home/End 会优先处理滚动,需要 polyfill 兜底(已在修复中,2026-07 后已修)
- 已有的 roving tabindex JS 要删掉。混用会导致双重处理,迁移时要把旧的
onkeydown监听器移除
三步下一步
- 在 Chrome/Edge 150+ 打开
about://flags→ Experimental Web Platform features,实战体验官方 demos - 审计项目里用了 roving tabindex 的组件(工具栏、Tab 栏、选项卡组),列出来准备迁移
- 先挑一个最简单的组件,用 polyfill 试跑,验证键盘行为和屏幕阅读器播报无误后再上生产
评论区
0 条评论
登录后可评论。