配了五年工具栏,每次加个按钮都要重写一整套键盘导航——今天一个 HTML 属性把这件事彻底原生化了
做过工具栏、Tab 列表、菜单的前端工程师,大概都写过或者抄过这套代码:监听方向键,find 当前焦点元素,update 下一个元素的 tabindex,call focus()。如果你的项目里没有这段,说明你们用了某个 UI 库把它封装掉了——但封装掉的代码依然是这段逻辑,只是你不用自己写。
这套模式在 ARIA Authoring Practices Guide 里叫 roving tabindex,是 WAI-ARIA 给复合组件的标准键盘交互方案。标准答案所有人都知道,但实现起来有几十个细节要处理:RTL 语言的方向键映射、disabled 和 hidden 元素要跳过、动态增删项目要维护索引、用户切走再切回来要恢复上次焦点……一个生产级的 roving tabindex 至少 40 行。
Chrome 150 和 Edge 150 把这件事彻底原生化了。一个 HTML 属性,全部替代。
focusgroup=”toolbar” 就能用
在容器上加 focusgroup=”toolbar”,里面所有可聚焦子元素就自动获得箭头键导航、单一 Tab 停靠、以及焦点记忆。不用写一行 JS:
<div focusgroup="toolbar wrap">
<button>Bold</button>
<button>Italic</button>
<button>Underline</button>
</div>
用户按 Tab 进入工具栏后,Left/Right 方向键在三个按钮之间循环切换(因为有 wrap),切走再 Tab 回来焦点会回到上次离开时的按钮。整个过程浏览器自动处理,不需要任何事件监听。
behavior token:toolbar / tablist / radiogroup / listbox / menu / menubar
focusgroup 的值是一个 behavior token,声明这个组件的类型。不同类型会获得不同的默认 ARIA 角色和键盘行为:
- toolbar:左右箭头键循环导航
- tablist:tabs 模式,当前 tab 独占 Tab 停靠,非活跃 tab 从无障碍树隐藏
- radiogroup:单选组,箭头键切换选中项
- listbox:列表框,上下左右导航
- menu / menubar:菜单,完整菜单键盘交互
modifier:inline / block / wrap / nowrap / nomemory
在 behavior token 后面可以加修饰符:
- inline / block:限制导航轴向。inline 只能用左右键,block 只能用上下键
- wrap / nowrap:wrap 导航到边缘后循环,nowrap 则停在边界
- nomemory:关闭焦点记忆,每次进组都从头开始
<!-- 垂直工具栏,不循环,不记忆 -->
<div focusgroup="toolbar block nowrap nomemory">
<button>Up</button>
<button>Down</button>
</div>
自动 ARIA 推断,但有限制
focusgroup 会自动给没有显式角色的子元素推断 ARIA role。focusgroup=”tablist” 里的 button 会自动获得 role=”tab”,父元素获得 role=”tablist”。这很省事,但有个前提:只有在元素没有自己语义的时候才会覆盖。
如果你用的是 <section> 而非 <div>,role 推断就不会发生,因为 section 有自己的语义。这时候你还是要手动补 role=”tablist”。
嵌套 focusgroup:菜单里装菜单
可以在一个 focusgroup 里面再声明一个 focusgroup。内层会创建独立的导航上下文,自动从外层的方向键导航中退出:
<div focusgroup="menubar">
<div focusgroup="menu">
<!-- 子菜单,独立的键盘导航 -->
</div>
</div>
这正是真实菜单系统的结构——菜单栏里套下拉菜单,focusgroup 把这个嵌套模式也原生支持了。
浏览器支持与 polyfill
Chrome 150+ / Edge 150+ 已稳定支持,约 45% 全球覆盖。Firefox 持积极态度(pending 实现),Safari 暂无信号。如果要在生产环境使用,可以用 @microsoft/focusgroup-polyfill 做渐进增强:
if (!(focusgroup in HTMLElement.prototype)) {
// 加载 polyfill
}
有 polyfill 的情况下,用户体验和原生支持完全一致——只是 polyfill 是 JS 模拟,效率不如浏览器原生,但功能完整。
一个属性的价值
focusgroup 替换掉的是一整类工程问题:每个 UI 库都在重复实现同一套键盘导航逻辑,每个团队都在维护同一份样板,每个无障碍审计都会挑出各种细节遗漏。浏览器把它收进去之后,这个问题从「你们自己处理」变成了「平台负责」,框架和业务代码都不需要再管。
这就是平台级 API 的意义——不是给你加一个新能力,是把所有人重复建设的那个东西消灭掉。
评论区
登录后可评论。