发布时间:2026 年 3 月 5 日
focusgroup HTML 属性是一种提议的声明性方式,用于向工具栏、标签页列表、菜单、列表框等复合 widget 添加键盘箭头键导航功能,而无需编写任何 roving-tabindex JavaScript。一个属性取代了数百行样板代码。我们希望在发布之前收到您的反馈。
欢迎试用并向我们提供反馈
您可以在 Chrome、Edge 和其他 Chromium 浏览器中尝试使用 focusgroup,只需通过以下两种方式之一启用它:
- 本地测试:在浏览器中,打开
about://flags页面并启用实验性 Web 平台功能标志。或者,使用--enable-blink-features=Focusgroup命令行参数从命令行启动浏览器。 - 源试用:注册参加 focusgroup 源试用,以便在您的网站上与真实用户一起测试该功能。
然后,探索互动式演示,了解每种模式的实际应用。
我们需要您的意见。提交焦点小组问题,告诉我们您的想法。
这是一项跨浏览器工作:该提案由 Microsoft 通过 OpenUI Community Group 提出,并获得了 Google 的大力支持。API 形态可能会根据您的反馈而发生变化。我们来深入了解一下 FocusGroup API 解决的问题以及该 API 的运作方式。
问题:手动漫游 tabindex
如果您曾构建过工具栏、tablist、菜单或列表框,那么您一定编写过某种版本的此代码。ARIA 创作实践指南 (APG) 建议复合 widget 呈现单个 Tab 键停止位置,并允许用户使用箭头键在各个项之间移动。这种模式称为“漫游 tabindex”。许多界面框架都从头重新实现了此功能:
<div role="toolbar" aria-label="Text formatting" id="toolbar">
<button type="button" tabindex="0">Bold</button>
<button type="button" tabindex="-1">Italic</button>
<button type="button" tabindex="-1">Underline</button>
<button type="button" tabindex="-1">Strikethrough</button>
</div>
接下来,开发者需要使用 JavaScript 来监听箭头键,以便移动焦点,并调整所有元素的 tabindex 属性。这是简化版本。生产实现还需要处理:
- 书写模式和 RTL:根据内容方向调整箭头键方向。
- 上次聚焦的内存:当用户按 Tab 键返回时,将焦点恢复到之前处于活动状态的项目。
- 已停用和已隐藏的项:在导航期间跳过这些项。
- 动态项:在添加或移除项时更新漫游索引。
大多数界面库(包括 React、Angular CDK 和 Fluent UI)都自带此逻辑的版本。这需要付出大量重复的努力才能获得本应是平台原语的东西。
解决方案:focusgroup 属性
使用 focusgroup 后,同一工具栏变为:
<div focusgroup="toolbar" aria-label="Text formatting">
<button type="button">Bold</button>
<button type="button">Italic</button>
<button type="button">Underline</button>
<button type="button">Strikethrough</button>
</div>
实时试用:工具栏模式 > 基本工具栏。 大功告成。没有用于箭头键导航的 JavaScript。无需手动管理 tabindex 。现在,浏览器会为您处理以下事宜:
- 箭头键导航:在各项之间导航,同时遵循书写模式和方向性。
- 单个 Tab 键停止位置:浏览器会自动将参与项折叠为一个 Tab 键停止位置。开发者无需对非有效商品设置
tabindex="-1"。 - 上次聚焦记忆:当用户离开焦点组并返回时,焦点会恢复到他们离开时所聚焦的项目。
- ARIA 语义:浏览器会根据使用通用元素时选择的行为提供适当的角色(例如
role="toolbar")。
开发者只需保留其功能特有的逻辑,例如切换按压状态、打开菜单、管理选择或任何自定义命令。
API 概览
focusgroup 属性接受以空格分隔的令牌列表。第一个令牌始终是声明 widget 模式的行为令牌。可选的修饰符令牌如下:focusgroup="<behavior> [inline|block] [wrap] [nomemory]"。
行为 token
行为令牌是必需的(除非使用 none 选择退出祖先焦点组)。它声明了复合 widget 模式,确保在未另行指定的情况下可以推断出正确的角色。这些令牌遵循 Aria 创作实践指南中所述的模式,并列在下表中:
| 行为 | APG 模式 | 最低容器角色(如果适用) | 最低子角色 (如果适用) |
默认修饰符 |
|---|---|---|---|---|
toolbar |
Google 工具栏 | 工具栏 | (无) | inline |
tablist |
APG 标签页 | tablist | 标签页 | inline wrap |
radiogroup |
单选按钮组 | radiogroup | 电台 | (无) |
listbox |
列表框 | 列表框 | option | (无) |
menu |
菜单 | 菜单 | menuitem | block wrap |
menubar |
菜单栏 | menubar | menuitem | inline wrap |
none |
无 | 无 | 无 | 无 |
如需详细了解角色映射的工作原理,请参阅说明。
轴限制(inline 和 block)
如果所选行为没有任何默认修饰键,则所有四个箭头键都可用于移动焦点。您可以使用 inline 或 block 修饰符将导航限制为单个逻辑轴:
inline:focusgroup 仅响应内联轴上的箭头键,在大多数英语语言环境中为向左和向右(从上到下的水平方向)。block:focusgroup 仅响应块轴上的箭头键,在大多数英语语言环境中为向上和向下(水平,从上到下)。
轴限制与 CSS 逻辑属性保持一致,并可自动适应书写模式和方向。
环绕式导航
默认情况下,箭头键导航会在 focusgroup 的边缘停止。添加 wrap 修饰符,以实现从最后一个商品循环回到第一个商品(以及从第一个商品循环回到最后一个商品)。如果某个行为默认具有环绕效果,请使用 nowrap 修饰符停用此行为。
实时试用:Tablist Pattern > Horizontal Tablist with Wrapping。 在该示例中,当焦点位于 FAQ 标签页上且用户按向右键时,焦点会返回到 Overview 标签页。
focusgroupstart 属性
focusgroupstart 属性用于标记在首次通过 Tab 键进入 focusgroup 时(或在停用内存时每次通过 Tab 键进入 focusgroup 时)哪个元素会获得焦点:
<div focusgroup="toolbar nomemory" aria-label="Entry point demo">
<button type="button">First</button>
<button type="button" focusgroupstart>Middle (Entry)</button>
<button type="button">Last</button>
</div>
Tab 和 Shift+Tab 都会停留在“中间(入口)”,因为该位置具有 focusgroupstart,并且内存已通过 nomemory 修饰符停用。实时试用:工具栏模式 > 带有 focusgroupstart 的入口点。
停用内存 (nomemory)
默认情况下,focusgroup 会记住上次获得焦点的项目,并在重新进入时通过 Tab 键恢复该项目。对于焦点应始终返回到固定入口点(如上一个演示中所示)的模式,请使用 focusgroup 属性中的 nomemory 修饰符将其停用。
此修饰符还可以与 focusgroupstart 的程序化移动相结合,让您在进入群组时完全控制聚焦的项目。当记忆的元素变得不可用时,系统会清除内存;例如,如果该元素被移除、隐藏、停用、处于非活动状态或从焦点组中排除,则会发生这种情况。
选择不启用(focusgroup="none")
使用 focusgroup="none" 可从祖先 focusgroup 的箭头导航中排除某个元素及其子树。选择不参与的元素及其子树仍可通过 Tab 键访问,但箭头键会跳过它们:
<div focusgroup="toolbar" aria-label="Segmented toolbar">
<button type="button">New</button>
<button type="button">Open</button>
<button type="button">Save</button>
<span focusgroup="none">
<button type="button">Help</button>
<button type="button">Shortcuts</button>
</span>
<button type="button">Close</button>
<button type="button">Exit</button>
</div>
使用向右键可依次前往“新建”“打开”“保存”“关闭”和“退出”,完全跳过“帮助”和“快捷方式”按钮。不过,用户仍然可以按 Tab 键进入帮助部分来访问这些按钮。实时试用:其他概念 > 包含 focusgroup="none" 的选择停用细分。
常见模式
Tablist
一种标签页控件,可在标签页之间使用箭头键进行导航。
<div focusgroup="tablist nomemory" aria-label="Sections">
<button type="button" aria-selected="true" aria-controls="panel-overview" id="tab-overview" focusgroupstart>Overview</button>
<button type="button" aria-selected="false" aria-controls="panel-features" id="tab-features">Features</button>
<button type="button" aria-selected="false" aria-controls="panel-pricing" id="tab-pricing">Pricing</button>
<button type="button" aria-selected="false" aria-controls="panel-faq" id="tab-faq">FAQ</button>
</div>
<div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview" tabindex="0">...</div>
<div role="tabpanel" id="panel-features" aria-labelledby="tab-features" tabindex="0">...</div>
<div role="tabpanel" id="panel-pricing" aria-labelledby="tab-pricing" tabindex="0">...</div>
<div role="tabpanel" id="panel-faq" aria-labelledby="tab-faq" tabindex="0">...</div>
实时试用:Tablist Pattern > Horizontal Tablist with Wrapping。
注意事项:
focusgroupstart属性位于已选择标签页上,因此焦点始终会进入该标签页。nomemory修饰符可确保即使之前用户曾聚焦于其他标签页,重新进入时也始终会进入所选标签页。inline修饰符将箭头键导航限制为仅限左右键。这与 APG Tabs 模式中概述的预期行为一致。- 借助
wrap修饰键,用户可以连续使用箭头键浏览所有标签页。 - 为简洁起见,此处省略了开发者代码,该代码用于处理实际选择:更新
aria-selected、切换面板显示状态,以及在选择发生变化时移动focusgroupstart属性。
菜单和菜单栏
一个简单的纵向菜单,包含向上和向下箭头导航。
<div focusgroup="menu" aria-label="File actions" class="menu-vertical">
<button type="button" class="menu-item">New</button>
<button type="button" class="menu-item">Open…</button>
<button type="button" class="menu-item">Save</button>
<button type="button" class="menu-item">Exit</button>
</div>
实时试用:菜单和菜单栏模式 > 简单垂直菜单。
使用 block 修饰符时,只有向上键和向下键可用于浏览项。向左键和向右键可自由用于您定义的行为(例如,打开子菜单)。对于具有嵌套子菜单的菜单栏,每个级别都是一个独立的 focusgroup。实时试用:
菜单和菜单栏模式 > 带有弹出式子菜单的菜单栏
<ul role="menubar" focusgroup="menubar"
aria-label="Application Menu" class="menubar">
<li role="none">
<button role="menuitem" type="button" class="menubar-item"
aria-haspopup="menu" aria-expanded="false"
popovertarget="filemenu">File</button>
<ul role="menu" focusgroup="menu"
id="filemenu" popover aria-label="File submenu" class="submenu">
<li role="none"><button type="button" class="submenu-item"
autofocus>New</button></li>
<li role="none"><button type="button" class="submenu-item">Open</button></li>
<li role="none"><button type="button" class="submenu-item">Save</button></li>
</ul>
</li>
<!-- More menu items... -->
</ul>
实时试用:
菜单和菜单栏模式 > 带有弹出式子菜单的菜单栏。
虽然菜单栏使用 inline 修饰符进行左右导航,但子菜单使用 block 修饰符进行上下导航。嵌套的 focusgroup 完全独立,因此不会相互干扰。
Radiogroup
一个自定义单选按钮组,支持使用箭头键进行导航,并可完全控制样式。
<div focusgroup="radiogroup" aria-label="Favorite color">
<span aria-checked="false" tabindex="0">Red</span>
<span aria-checked="false" tabindex="0">Green</span>
<span aria-checked="true" tabindex="0" focusgroupstart >Blue</span>
<span aria-checked="false" tabindex="0">Purple</span>
</div>
实时试用:单选按钮组模式 > 比较:原生与 Focusgroup。
虽然 focusgroup 属性可处理箭头键导航,但您必须实现选择代码。在此演示中,JavaScript 代码通过使用 aria-checked 属性来管理选中状态。
主要概念
焦点小组项目参与情况
如果元素的 focusgroup 设置为有效行为,则该元素的所有可按顺序聚焦的后代都被视为参与该焦点组。这意味着,系统不会考虑 tabindex 为负值的元素,但会考虑原生可聚焦元素(例如 <button>)以及您指定了非负 tabindex 的元素。
制表位
您无需管理 tabindex 值。即使多个后代元素自然可使用 Tab 键切换(例如,多个 <button> 元素),focusgroup 也会将它们折叠为单个 Tab 键停止位置。浏览器会处理在任何给定时间哪个项可使用 Tab 键选择。实时试用:工具栏模式 > 无需管理 tabindex。
上次聚焦的内存
默认情况下,当用户按 Tab 键离开焦点组,然后再次按 Tab 键返回时,焦点会返回到上次获得焦点的项目。这对于大型列表和工具栏至关重要,可确保用户不会丢失位置。如果您希望焦点始终恢复到第一个元素,或者在使用 focusgroupstart 时控制初始聚焦的元素,请使用 nomemory 修饰符来停用此行为。
嵌套焦点小组
每个 focusgroup 声明都会创建一个独立的作用域。嵌套的 focusgroup 会自动选择不使用其祖先的箭头导航。使用 Tab 键可在焦点组之间移动,使用箭头键可在当前焦点组内移动。 实时试用:其他概念 > 嵌套的 Focusgroup。
Shadow DOM 支持
默认情况下,focusgroup 会跨 shadow DOM 边界应用。在影子宿主上声明的 focusgroup 包含该宿主的影子树内的可聚焦元素。如果您想选择停用,可以在组件的影子树中使用 focusgroup="none"。
键冲突处理
focusgroup 内的某些元素(例如 <input>、<textarea> 和其他控件)会出于自身目的使用箭头键。当 focusgroup 的导航键与原生元素的箭头键行为发生冲突时:
- 箭头键由互动元素使用(例如,用于移动文本光标),focusgroup 不会干扰。
- Tab 或 Shift+Tab 提供了一种默认的退出机制,允许用户使用 Tab 键导航“重新进入”焦点组。
这些转义行为仅在存在实际键冲突时适用;非冲突轴不受影响。您还可以针对 keydown 事件调用 preventDefault(),以针对特定元素替换焦点组的箭头键行为。这意味着,您可以在 focusgroup 中包含输入和 textarea,而不会破坏任何行为。
如果您向参与 focusgroup 的自有元素添加了按键处理程序,请务必提供类似的退出机制,以便用户访问群组的其余部分。
深层后代发现
焦点组项不必是焦点组容器的直接子级。
浏览器会将所有可按顺序聚焦的后代元素(非负 tabindex)纳入焦点组,除非这些元素位于嵌套的焦点组中或通过 focusgroup="none" 选择不纳入焦点组。
<div focusgroup="toolbar" aria-label="Nested wrappers">
<div>
<span>
<button type="button">Alpha</button>
</span>
<span>
<button type="button">Beta</button>
</span>
<span>
<button type="button">Gamma</button>
</span>
</div>
</div>
即使按钮嵌套在 <div> 和 <span> 封装容器中,箭头键导航也能正常运行。没有对扁平列表的要求,因此用于设置样式的封装元素没问题。
实时试用:其他概念 > 深层后代。
与 reading-flow 媒体资源集成
无论是顺序(Tab 键)导航还是方向(箭头键)导航,在存在 CSS reading-flow 属性时,都会遵循该属性,按照视觉阅读顺序而非 DOM 源顺序进行导航。
这可确保箭头键导航与用户在屏幕上看到的布局相符。
<div focusgroup="toolbar" aria-label="Visual order"
style="display: flex; flex-direction: row-reverse; reading-flow: flex-visual;">
<button type="button">A (DOM first)</button>
<button type="button">B (DOM second)</button>
<button type="button">C (DOM third)</button>
</div>
虽然 DOM 顺序为 A、B、C,但由于布局使用 flex-direction: row-reverse,因此视觉顺序为 C、B、A。不过,由于代码还使用了 reading-flow: flex-visual,因此读取顺序恢复为 A、B、C,并且 focusgroup 与此顺序一致。
按 Tab 键会先将焦点移至 C,然后按向右键会将焦点移至 B,再移至 A。 实时试用:其他概念 > CSS 阅读流集成。
无障碍
ARIA 角色推理
在焦点组中,浏览器会使用行为令牌来推断容器及其参与项的最低角色。这意味着,当为具有通用角色的元素设置 focusgroup 属性时,系统会根据所选行为应用正确的角色。具有通用角色的元素参与项或没有指定角色的按钮将相应地推断出其角色。例如,以下 HTML:
<div focusgroup="tablist">
<button>Tab 1</button>
<button>Tab 2</button>
<button>Tab 3</button>
</div>
即使按钮上未定义任何角色,也会创建以下无障碍功能树:
+ tablist
|
+ tab
|
+ tab
|
+ tab
您始终可以通过直接设置角色来控制行为。
无障碍注意事项
请务必遵守您在创建焦点组时选择的行为。
焦点群组的使用情况应尽可能与您指定的行为保持一致。这对于确保依赖无障碍工具的用户能够浏览内容和使用自定义控件至关重要。
虽然角色推理功能提供了良好的默认设置,但在使用具有非通用角色的元素时,请务必确保这些元素具有适当的角色集,以实现其提供的功能。
使用 focusgroup 时,请注意用户可能需要能够使用箭头键滚动来查看您的内容。键盘用户应始终能够读取和访问您网页上的内容。
功能检测
如需立即开始使用 focusgroup,在浏览器完全支持该功能之前,您可以在 JavaScript 中检测 focusgroup 支持情况:
if ('focusgroup' in HTMLElement.prototype) {
// focusgroup is supported.
} else {
// fall back to manual roving tabindex.
}
总结
focusgroup 属性正在通过标准机构的审核,我们也在积极构建 Chromium 中的原型并完善 API。
请试用一下,并在 Open-UI GitHub 问题跟踪器中提交焦点组问题。我们尤其想了解您对以下方面的看法:
- API Surface 是否适合您构建的模式?
- 我们是否遗漏了某些模式或场景?
- 是否有不允许使用 focusgroup 属性的元素?
- 无障碍功能故事如何适用于您的使用情形?
感谢您帮助我们改进了网页上的键盘导航功能!
了解详情
感谢 Mason Freed、Sara Higley、Scott O'Hara 和 Open-UI 社区的其他成员在帮助我们恢复 focusgroup 方面所做的贡献。