构建无障碍组件之Toolbar Pattern

作者:anOnion日期:2026/5/25

Toolbar Pattern 详解:构建无障碍的工具栏组件

Toolbar(工具栏)是一种用于组合一组控件的容器,例如按钮、菜单按钮或复选框。本文基于 W3C WAI-ARIA Toolbar Pattern 规范,详解如何构建无障碍的工具栏组件。

一、Toolbar 的定义与核心概念

1.1 什么是 Toolbar

Toolbar 是一种控件分组容器,具有以下特征:

  • 将一组相关控件(按钮、菜单、复选框等)视觉上分组
  • 通过 role="toolbar" 向屏幕阅读器用户传达分组的存在和目的
  • 减少键盘 Tab 序列中的停靠点数量
  • 使用方向键在控件之间导航
  • 通常包含 3 个或更多控件

1.2 核心术语

术语说明
Toolbar Container工具栏容器,包含所有控件
Control工具栏内的控件(按钮、菜单等)
Roving Tabindex流动 Tab 索引,管理工具栏内的焦点
Orientation工具栏方向(水平或垂直)
1┌─────────────────────────────────────────────────────────────┐
2                                                             
3  ┌─────────────────────────────────────────────────────┐    
4                                                           
5    ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐           
6     Bold  │Italic│ │Under-│ │Align  │Font             
7      B      I    │line U│ │Left   │Size             
8    └──────┘ └──────┘ └──────┘ └──────┘ └──────┘           
9                                                           
10    role="toolbar"                                         
11    aria-label="Formatting"                                
12                                                           
13    Tab: Enter/Exit Toolbar                                
14      : Move Focus Between Controls                      
15                                                           
16  └─────────────────────────────────────────────────────┘    
17                                                             
18└─────────────────────────────────────────────────────────────┘
19

1.3 典型应用场景

  • 文本编辑器工具栏:粗体、斜体、下划线、对齐等按钮
  • 富文本编辑器:字体选择、颜色选择、插入链接等
  • 媒体播放器:播放、暂停、音量、进度控制
  • 绘图工具:画笔、橡皮擦、颜色选择器
  • 表格操作:插入行、删除列、合并单元格

二、WAI-ARIA 角色与属性

2.1 基本角色

Toolbar 使用 role="toolbar" 标记容器。

1<div
2  role="toolbar"
3  aria-label="格式工具栏">
4  <button aria-pressed="false">粗体</button>
5  <button aria-pressed="false">斜体</button>
6  <button>下划线</button>
7</div>
8

2.2 必需属性

属性说明示例值
role="toolbar"标记工具栏容器-
aria-label / aria-labelledby工具栏标签"格式工具栏"

2.3 可选属性

属性说明示例值
aria-orientation工具栏方向"horizontal"(默认), "vertical"

2.4 属性详解

aria-orientation

用于指定工具栏的方向:

1<!-- 水平工具栏(默认) -->
2<div
3  role="toolbar"
4  aria-label="格式工具栏">
5  ...
6</div>
7
8<!-- 垂直工具栏 -->
9<div
10  role="toolbar"
11  aria-label="侧边工具栏"
12  aria-orientation="vertical">
13  ...
14</div>
15

注意

  • 默认方向为水平(horizontal
  • 垂直工具栏需要显式设置 aria-orientation="vertical"
  • 方向影响键盘导航的行为

三、键盘交互规范

3.1 基本键盘交互

按键功能
Tab将焦点移入工具栏
Shift + Tab将焦点移出工具栏
Left Arrow将焦点移到上一个控件(水平工具栏)
Right Arrow将焦点移到下一个控件(水平工具栏)
Up Arrow将焦点移到上一个控件(垂直工具栏)
Down Arrow将焦点移到下一个控件(垂直工具栏)
Home(可选)将焦点移到第一个控件
End(可选)将焦点移到最后一个控件

3.2 焦点管理

首次进入工具栏
  • 焦点设置到第一个非禁用控件
再次进入工具栏
  • 焦点可选地设置到之前获得焦点的控件
  • 否则,焦点设置到第一个非禁用控件
水平工具栏导航
1┌─────────────────────────────────────────────────────────┐
2                                                         
3  ┌──────┐    ┌──────┐    ┌──────┐    ┌──────┐          
4   Bold  ←→ │Italic│ ←→ │Under-│ ←→ │Font            
5    B         I       │line U│    │Size            
6  └──────┘    └──────┘    └──────┘    └──────┘          
7                                                         
8  Left Arrow: Previous Control                           
9  Right Arrow: Next Control                              
10  (Optional: Focus Wraps from First/Last)                
11                                                         
12└─────────────────────────────────────────────────────────┘
13
垂直工具栏导航
1┌─────────────────────────┐
2                         
3  ┌──────┐               
4   Bold                
5    B                  
6  └──────┘               
7     ↑↓                  
8  ┌──────┐               
9  │Italic│               
10    I                  
11  └──────┘               
12     ↑↓                  
13  ┌──────┐               
14  │Under-│               
15  │line U│               
16  └──────┘               
17                         
18  Up Arrow: Previous     
19  Down Arrow: Next       
20                         
21└─────────────────────────┘
22

3.3 方向键的复用

在水平工具栏中:

  • Left/Right Arrow:在控件间导航
  • Up/Down Arrow:可以复用 Left/Right Arrow 的功能,或保留给需要垂直方向键操作的控件(如 Spinbutton)

在垂直工具栏中:

  • Up/Down Arrow:在控件间导航
  • Left/Right Arrow:可以复用 Up/Down Arrow 的功能,或保留给需要水平方向键操作的控件(如 Slider)

3.4 禁用控件的焦点

通常,禁用控件在键盘导航中不可聚焦。但在某些情况下,如果发现功能至关重要,可以让禁用控件可聚焦,以便屏幕阅读器用户了解其存在。

四、实现方式

4.1 基础 Toolbar 结构

1<div
2  role="toolbar"
3  aria-label="文本格式工具栏">
4  <button
5    id="bold-btn"
6    aria-pressed="false"
7    tabindex="0">
8    粗体
9  </button>
10  <button
11    id="italic-btn"
12    aria-pressed="false"
13    tabindex="-1">
14    斜体
15  </button>
16  <button
17    id="underline-btn"
18    tabindex="-1">
19    下划线
20  </button>
21  <button
22    id="align-left-btn"
23    aria-pressed="true"
24    tabindex="-1">
25    左对齐
26  </button>
27  <button
28    id="align-center-btn"
29    aria-pressed="false"
30    tabindex="-1">
31    居中对齐
32  </button>
33</div>
34

4.2 JavaScript 实现

1class Toolbar {
2  constructor(element) {
3    this.toolbar = element;
4    this.controls = Array.from(
5      this.toolbar.querySelectorAll(
6        'button, [role="button"], input, select, [role="checkbox"], [role="radio"]',
7      ),
8    );
9    this.orientation =
10      this.toolbar.getAttribute('aria-orientation') || 'horizontal';
11
12    this.init();
13  }
14
15  init() {
16    // 键盘事件
17    this.toolbar.addEventListener('keydown', this.handleKeyDown.bind(this));
18
19    // 焦点管理
20    this.controls.forEach((control, index) => {
21      control.addEventListener('focus', () => this.setFocusedControl(index));
22    });
23  }
24
25  handleKeyDown(e) {
26    const currentIndex = this.controls.indexOf(document.activeElement);
27
28    if (currentIndex === -1) return;
29
30    let nextIndex = -1;
31
32    switch (e.key) {
33      case 'ArrowLeft':
34        if (this.orientation === 'horizontal') {
35          e.preventDefault();
36          nextIndex = this.getPreviousIndex(currentIndex);
37        }
38        break;
39      case 'ArrowRight':
40        if (this.orientation === 'horizontal') {
41          e.preventDefault();
42          nextIndex = this.getNextIndex(currentIndex);
43        }
44        break;
45      case 'ArrowUp':
46        if (this.orientation === 'vertical') {
47          e.preventDefault();
48          nextIndex = this.getPreviousIndex(currentIndex);
49        }
50        break;
51      case 'ArrowDown':
52        if (this.orientation === 'vertical') {
53          e.preventDefault();
54          nextIndex = this.getNextIndex(currentIndex);
55        }
56        break;
57      case 'Home':
58        e.preventDefault();
59        nextIndex = 0;
60        break;
61      case 'End':
62        e.preventDefault();
63        nextIndex = this.controls.length - 1;
64        break;
65    }
66
67    if (nextIndex !== -1) {
68      this.focusControl(nextIndex);
69    }
70  }
71
72  getPreviousIndex(currentIndex) {
73    // 循环到末尾
74    return currentIndex === 0 ? this.controls.length - 1 : currentIndex - 1;
75  }
76
77  getNextIndex(currentIndex) {
78    // 循环到开头
79    return currentIndex === this.controls.length - 1 ? 0 : currentIndex + 1;
80  }
81
82  focusControl(index) {
83    const control = this.controls[index];
84    if (control && !control.disabled) {
85      control.focus();
86    }
87  }
88
89  setFocusedControl(index) {
90    // 更新 tabindex,实现 roving tabindex
91    this.controls.forEach((control, i) => {
92      control.setAttribute('tabindex', i === index ? '0' : '-1');
93    });
94  }
95}
96
97// 初始化
98const toolbar = document.querySelector('[role="toolbar"]');
99new Toolbar(toolbar);
100

4.3 富文本编辑器 Toolbar 示例

1<div
2  role="toolbar"
3  aria-label="富文本编辑器工具栏">
4  <!-- 文本样式 -->
5  <button
6    id="bold"
7    aria-pressed="false"
8    tabindex="0">
9    <span aria-hidden="true">B</span>
10    <span class="sr-only">粗体</span>
11  </button>
12  <button
13    id="italic"
14    aria-pressed="false"
15    tabindex="-1">
16    <span aria-hidden="true">I</span>
17    <span class="sr-only">斜体</span>
18  </button>
19  <button
20    id="underline"
21    aria-pressed="false"
22    tabindex="-1">
23    <span aria-hidden="true">U</span>
24    <span class="sr-only">下划线</span>
25  </button>
26
27  <!-- 分隔符 -->
28  <span
29    role="separator"
30    aria-orientation="vertical"></span>
31
32  <!-- 对齐方式 -->
33  <button
34    id="align-left"
35    aria-pressed="true"
36    tabindex="-1">
37    <span class="sr-only">左对齐</span>
38  </button>
39  <button
40    id="align-center"
41    aria-pressed="false"
42    tabindex="-1">
43    <span class="sr-only">居中对齐</span>
44  </button>
45  <button
46    id="align-right"
47    aria-pressed="false"
48    tabindex="-1">
49    <span class="sr-only">右对齐</span>
50  </button>
51
52  <!-- 分隔符 -->
53  <span
54    role="separator"
55    aria-orientation="vertical"></span>
56
57  <!-- 字体大小 -->
58  <label for="font-size">字体大小</label>
59  <select
60    id="font-size"
61    tabindex="-1">
62    <option value="12">12px</option>
63    <option value="14">14px</option>
64    <option
65      value="16"
66      selected>
67      16px
68    </option>
69    <option value="18">18px</option>
70  </select>
71</div>
72

4.4 垂直 Toolbar 示例

1<div
2  role="toolbar"
3  aria-label="绘图工具栏"
4  aria-orientation="vertical">
5  <button
6    id="brush"
7    aria-pressed="true"
8    tabindex="0">
9    <span class="sr-only">画笔</span>
10  </button>
11  <button
12    id="eraser"
13    aria-pressed="false"
14    tabindex="-1">
15    <span class="sr-only">橡皮擦</span>
16  </button>
17  <button
18    id="line"
19    aria-pressed="false"
20    tabindex="-1">
21    <span class="sr-only">直线</span>
22  </button>
23  <button
24    id="rectangle"
25    aria-pressed="false"
26    tabindex="-1">
27    <span class="sr-only">矩形</span>
28  </button>
29  <button
30    id="circle"
31    aria-pressed="false"
32    tabindex="-1">
33    <span class="sr-only">圆形</span>
34  </button>
35</div>
36

五、最佳实践

5.1 控件数量

仅在包含 3 个或更多控件时使用 Toolbar

1<!-- 好的示例:3 个以上控件 -->
2<div
3  role="toolbar"
4  aria-label="格式工具栏">
5  <button>粗体</button>
6  <button>斜体</button>
7  <button>下划线</button>
8</div>
9
10<!-- 不好的示例:控件太少,不需要 Toolbar -->
11<div
12  role="toolbar"
13  aria-label="操作">
14  <button>保存</button>
15</div>
16

5.2 避免冲突的控件

避免包含需要与 Toolbar 导航方向冲突的控件

1<!-- 错误:水平工具栏中包含需要左右方向键的 Spinbutton -->
2<!-- 问题:用户按 Left/Right 时,Toolbar  Spinbutton 都要响应,冲突! -->
3<div
4  role="toolbar"
5  aria-label="错误示例">
6  <button>加粗</button>
7  <button>斜体</button>
8  <div
9    role="spinbutton"
10    aria-label="字体大小"
11    aria-valuenow="16">
12    16px
13  </div>
14  <!-- 冲突! -->
15</div>
16
17<!-- 正确:避免使用方向键冲突的控件,或改用不冲突的替代方案 -->
18<div
19  role="toolbar"
20  aria-label="正确示例">
21  <button>加粗</button>
22  <button>斜体</button>
23  <!--  select 替代 spinbutton:select 只有展开后才占用方向键,平时不冲突 -->
24  <select aria-label="字体大小">
25    <option>12px</option>
26    <option selected>16px</option>
27    <option>20px</option>
28  </select>
29</div>
30

5.3 使用 Roving Tabindex

通过 Roving Tabindex 管理焦点,确保只有一个控件在 Tab 序列中:

1// 初始化时,只有第一个控件 tabindex="0"
2// 其他控件 tabindex="-1"
3
4// 当焦点移动时,更新 tabindex
5setFocusedControl(index) {
6  this.controls.forEach((control, i) => {
7    control.setAttribute('tabindex', i === index ? '0' : '-1');
8  });
9}
10

5.4 提供快捷键

在需要快速访问工具栏的应用中,提供快捷键:

1// 从文本区域快速跳转到工具栏
2document.addEventListener('keydown', (e) => {
3  if (e.altKey && e.key === 'f10') {
4    e.preventDefault();
5    const toolbar = document.querySelector('[role="toolbar"]');
6    const firstControl = toolbar.querySelector('[tabindex="0"]');
7    firstControl.focus();
8  }
9});
10

5.5 视觉与语义一致

确保工具栏的视觉呈现与 ARIA 语义一致:

1<!-- 视觉上是分组,语义上也是分组 -->
2<div
3  role="toolbar"
4  aria-label="对齐工具栏"
5  class="toolbar-group">
6  <button aria-pressed="true">左对齐</button>
7  <button aria-pressed="false">居中对齐</button>
8  <button aria-pressed="false">右对齐</button>
9</div>
10

5.6 处理禁用控件

1<!-- 禁用控件通常不可聚焦 -->
2<button disabled>不可用</button>
3
4<!-- 但在需要发现功能时,可以让其可聚焦 -->
5<button
6  aria-disabled="true"
7  tabindex="-1">
8  即将推出
9</button>
10

六、常见错误

6.1 忘记设置 role="toolbar"

1<!-- 错误 -->
2<div class="toolbar">
3  <button>粗体</button>
4  <button>斜体</button>
5</div>
6
7<!-- 正确 -->
8<div
9  role="toolbar"
10  aria-label="格式工具栏">
11  <button>粗体</button>
12  <button>斜体</button>
13</div>
14

6.2 所有控件都 tabindex="0"

1<!-- 错误:所有控件都在 Tab 序列中 -->
2<div role="toolbar">
3  <button tabindex="0">按钮 1</button>
4  <button tabindex="0">按钮 2</button>
5  <button tabindex="0">按钮 3</button>
6</div>
7
8<!-- 正确:只有一个控件在 Tab 序列中 -->
9<div role="toolbar">
10  <button tabindex="0">按钮 1</button>
11  <button tabindex="-1">按钮 2</button>
12  <button tabindex="-1">按钮 3</button>
13</div>
14

6.3 控件太少使用 Toolbar

1<!-- 错误:只有 2 个控件 -->
2<div
3  role="toolbar"
4  aria-label="操作">
5  <button>保存</button>
6  <button>取消</button>
7</div>
8
9<!-- 正确:直接放置,不使用 Toolbar -->
10<button>保存</button>
11<button>取消</button>
12

6.4 方向键冲突

1<!-- 错误:水平工具栏中包含需要左右方向键的控件 -->
2<div
3  role="toolbar"
4  aria-label="错误示例">
5  <button>按钮</button>
6  <div
7    role="spinbutton"
8    aria-label="字体大小">
9    16
10  </div>
11  <!-- 冲突! -->
12</div>
13

七、Toolbar vs 其他组件

7.1 Toolbar vs Menu

特性ToolbarMenu
结构平级控件层级结构
交互直接操作选择后执行
持久性始终可见通常需要触发
典型用例格式工具栏下拉菜单

7.2 Toolbar vs Button Group

特性ToolbarButton Group
键盘导航方向键Tab 键
焦点管理Roving Tabindex独立焦点
Tab 序列一个停靠点多个停靠点
适用场景频繁操作的工具相关操作的分组

八、总结

构建无障碍的 Toolbar 组件需要关注:

  1. 正确的角色:使用 role="toolbar"
  2. 标签:使用 aria-labelaria-labelledby
  3. 方向:使用 aria-orientation 指定方向
  4. 焦点管理:使用 Roving Tabindex 减少 Tab 停靠点
  5. 键盘导航:方向键在控件间移动焦点
  6. 控件数量:仅在 3 个以上控件时使用
  7. 避免冲突:避免包含与导航方向冲突的控件
  8. 快捷键:在需要快速访问时提供快捷键

遵循 W3C Toolbar Pattern 规范,我们能够创建既实用又无障碍的工具栏组件,为所有用户提供高效的操作体验。

文章同步于 an-Onion 的 Github。码字不易,欢迎点赞。


构建无障碍组件之Toolbar Pattern》 是转载文章,点击查看原文


相关推荐


Android 窗口容器树(一)—— 窗口和窗口容器树
无限进化2026/5/4

窗口容器树系列文章: Android 窗口容器树(一)—— 窗口和窗口容器树 Android 窗口容器树(二)—— 窗口容器树的构建 1. 什么是窗口? 在 Android 中,窗口不是 View,也不是某个单独的界面控件,而是系统层面对一块可显示 UI 内容的抽象管理单元。 它至少有 3 个核心特征: 它有独立的 WindowManager.LayoutParams,用来描述类型、位置、大小、标志位等。 它最终会对应到 SurfaceControl / Surface,并交给 Surfa


【从0开始学设计模式-11| 外观模式】
我爱cope2026/4/24

概念 外观模式(Facade Pattern) 是一种使用频率非常高的结构型设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。 定义: 为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子


VPS 买回来第一天该干什么?我的开机必做清单
小墨同学boy2026/4/16

很多人买来第一台VPS可能不知道干什么,是到手之后直接装宝塔,然后部署一个WordPress 博客丢上去跑了两天。可能过几天你就会发现 auth.log 里全是暴力破解记录,SSH 端口没有改还是默认的 22,root 密码没有做修改还是简单的 8 数字。最离谱的是你没有一开始测试号,最后发现那台机器的网络在晚高峰根本不能用,到最后可以部署完了才发现的,前面装的东西全白费功夫。 所以我才要写这篇文章,介绍我的流程:新机器到手,不要一上来就按照服务,先按照我的固定流程把去设置一遍服务器。整个过程其


# KubeBlocks for MSSQL 高可用实现
小猿姐2026/4/8

背景 Microsoft SQL Server(MSSQL)是由微软开发的一款关系型数据库管理系统。最初仅支持在 Windows 平台上运行,自 2017 版本起开始支持 Linux 系统,这一变化为 MSSQL 的容器化部署提供了可能。 MSSQL 提供了名为 Availability Group(可用性组,下文简称AG) 的多数据库复制管理特性,该特性支持在多个节点上实现数据库的多副本冗余,从而提升数据可靠性和服务连续性。在 Windows 平台上,MSSQL 通过与 Windows Ser


AI时代,我们的任务不应沉溺于与 AI 聊天 - 🤔 从“对话式编程”迈向“数字软件工厂”
阿文WUTAI感话2026/3/31

2026年,AI辅助开发的正确打开方式 在 2026 年的今天,研发工程师已经意识到,AI 辅助开发不应只是零散的提示词,而是一套 有标准、有性能、有角色、有流程 的系统工程 。通过 OpenSpec、Everything Claude Code (ECC)、gstack 以及新增的 superpowers 的深度协同,我们可以构建起一套现代化的"数字软件工厂",让研发协作工作流与状态流转更加符合项目的确定性需求 一、核心概念:先对齐术语,避免鸡同鸭讲 要驾驭这套工厂,首先需要对齐底层的专业概


家政服务小程序预约上门服务维修保洁上门服务在线派单技师入驻-ym7K
2601_952013762026/3/22

一、后台管理端核心功能 1. 系统基础配置 提供基础设置、店铺管理、家政管理三大核心配置入口,支撑系统整体运行。支持家政分类管理,可灵活划分保洁、月嫂、家电维修等服务类型。 2. 家政服务发布与管理 发布家政服务: 选择对应家政分类,填写服务标题。配置所在区域、家政公司名称、联系人及联系电话。设定家政性质:免费预约、预约金、实价三种模式可选。通过富文本编辑器编辑服务详情,支持图文排版。 管理家政:对已发布服务进行编辑、上下架等操作。管理订单:查看、处理用户预约订单,跟踪服务进度。服


Codex 工程化实践指南:深入理解 AGENTS.md、SKILL.md 与 MCP
Lei_official2026/3/14

AI 就像自动驾驶,其价值并非让没摸过方向盘的新手上路开车,而在于为熟练的驾驶者节约精力和时间。 在 Codex 的设计中,有三个非常关键的概念: AGENTS.md SKILL.md MCP(Model Context Protocol) 如果把 Codex 看成一个 “AI 工程师”,那么这三个概念相当于: 概念角色AGENTS.md团队开发规范SKILL.md可复用工作流MCP外部系统接口 注意这里的“团队开发规范”不是指人类工程师所组成


端侧RAG实战指南
稀有猿诉2026/3/6

本文译自「On-Device RAG for App Developers: Embeddings, Vector Search, and Beyond」,原文链接medium.com/google-deve…,由Sasha Denisov发布于2026年2月21日。 我们已经探讨了离线 AI 代理的重要性 和如何通过函数调用赋予它们工具。现在,让我们通过赋予它们记忆——即使用 RAG(检索增强生成)搜索和检索你的私有数据——来完善整个图景。 当我开始构建 Flutter Gemma 时,


Gemini 3.1 Pro 正式发布:一次低调更新,还是谷歌的关键反击?
IvanCodes2026/2/25

今天凌晨,谷歌发布了新一代模型——Gemini 3.1 Pro 没有大型发布会,没有提前预热,甚至连宣传节奏都显得克制。 很多人会把它看作 Gemini 3 的小版本升级,但从目前披露的测试数据和演示能力来看,这更像是一次结构性强化,而不是简单的参数迭代。 如果说 Gemini 3 是谷歌重新回到核心竞争区间的标志,那么 Gemini 3.1 Pro,则明显带着更强的实战优化意味。 它在几个关键方向上给出了非常明确的信号:谷歌不只是追赶者。 性能升级:从可用到强势竞争 这次升


React 性能优化:图片懒加载
NEXT062026/2/17

引言 在现代 Web 应用开发中,首屏加载速度(FCP)和最大内容绘制(LCP)是衡量用户体验的核心指标。随着富媒体内容的普及,图片资源往往占据了页面带宽的大部分。如果一次性加载页面上的所有图片,不仅会阻塞关键渲染路径,导致页面长时间处于“白屏”或不可交互状态,还会浪费用户的流量带宽。 图片懒加载(Lazy Loading)作为一种经典的性能优化策略,其核心思想是“按需加载”:即只有当图片出现在浏览器可视区域(Viewport)或即将进入可视区域时,才触发网络请求进行加载。这一策略能显著减少首屏

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读