关于 keyboard
keyboard 是面向 UI 实现与审查的键盘可访问性技能,涵盖交互控件、焦点管理和复合组件的操作规则。它强调原生控件、符合预期的按键行为及实际测试。
主要功能
- 01键盘可达与操作要求
- 02常见控件按键规范
- 03对话框焦点管理
- 04焦点可见性与顺序
- 05复合组件键盘交互
适用场景
- 01检查交互控件能否仅用键盘操作
- 02审查模态对话框的焦点行为
- 03改进焦点指示器与 Tab 顺序
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mgifford/ accessibility-skills --skill keyboard安装后在 Claude Code 中输入/keyboard确认已加载。
附带文件SYNC.md
详细介绍
键盘无障碍技能
规范来源:
mgifford/ACCESSIBILITY.md中的examples/KEYBOARD_ACCESSIBILITY_BEST_PRACTICES.md此技能源自该文件。如有疑问,以该示例为准。
对每个交互式 UI 元素和功能应用这些规则。
核心要求
所有交互功能都必须仅用键盘即可完全操作——无需鼠标或触摸,除非底层功能确实依赖基于路径的移动。键盘访问并不等同于通过每个控件按 Tab 键——原生控件和复合控件使用不同的按键约定,用户必须了解如何操作自定义控件。
严重程度等级(本技能)
| 等级 | 含义 |
|---|---|
| 严重 | 完全阻碍键盘用户和 AT 用户完成任务 |
| 较严重 | 显著影响键盘访问;无法接受的解决方法 |
| 中等 | 给键盘用户带来不便;存在替代方法 |
| 轻微 | 未达到最佳实践;对键盘使用的影响较小 |
不要仅根据规则本身判定严重程度——应确定实际任务影响。考虑缺陷是否会阻止用户到达、操作或离开某个组件;是否会阻碍核心任务,还是仅影响可选功能;焦点是否丢失或被误导性地放置;以及是否仍有合理的键盘操作方式。
严重:不得设置键盘陷阱
如果焦点可以进入某个组件,用户就必须能够根据组件情况,通过 Tab、Shift+Tab、方向键或 Escape 将焦点移出。如果离开组件需要不常见的命令,应在组件之前或组件内提供说明。
无法退出的键盘陷阱属于严重问题。 如果模态对话框提供标准或有说明的退出方式,则临时限制焦点并不属于陷阱:打开时焦点移入其中,按 Tab/Shift+Tab 时焦点留在其中,有可通过键盘操作的控件用于关闭或完成操作,并且关闭后焦点返回到触发控件或其他符合逻辑的位置。
请仔细测试嵌入式编辑器、第三方控件、iframe、媒体播放器、远程桌面和 canvas 应用——它们通常会拦截 Tab 或其他按键,因此需要提供可用且有说明的退出方式。
严重:所有交互元素都必须可通过键盘访问
所有可通过鼠标激活的元素都必须可通过键盘到达并激活。使用原生元素——它们内置了键盘行为、焦点能力、语义、状态和浏览器集成,无需额外成本:
<button type="button">Save</button>
<a href="/about">About</a>
<div role="button" tabindex="0">Save</div>
仅添加 role="button" 和 tabindex="0" 并不会自动赋予按钮行为——自定义按钮还必须实现 Enter 和 Space 激活、禁用状态、焦点样式以及预期的可访问名称和状态。测试实际效果,而不只是检查是否存在 keydown 处理程序。不得仅通过鼠标和触摸处理程序操作控件。
关键:预期的按键行为
偏离预期的控件按键行为属于关键问题——这会破坏辅助技术用户所依赖的心智模型。除非有已记录的必要理由,否则不要覆盖原生控件的标准浏览器行为。对于自定义控件,请遵循当前的 WAI-ARIA APG(其版本独立于 WCAG;APG 模式本身属于设计指南,而非 WCAG 要求;请始终确认当前版本)。
| 控件 | 必需按键 |
|---|---|
| 按钮 | Enter、Space |
| 链接 | Enter |
| 复选框 | 按 Space 切换 |
| 单选按钮组 | 使用方向键在选项间移动;按 Space 进行选择 |
<select> | 遵循浏览器/平台的导航和选择惯例 |
| 菜单/菜单栏 | 使用方向键;按 Enter 激活项目;按 Escape 关闭 |
| 选项卡控件 | 使用方向键在选项卡间移动;按 Tab 移入当前面板 |
| 对话框 | 按 Escape 关闭;打开时将焦点限制在对话框内 |
| 组合框 | 取决于可编辑或仅可选择的行为;通常按 Escape 关闭 |
| 树视图 | 使用向上/向下键浏览可见节点;向右键展开/进入;向左键折叠/返回父节点 |
| 滑块 | 使用方向键调整值;按 Home/End 跳至最小值/最大值;按 Page Up/Page Down 进行较大步进 |
| 网格 | 使用方向键在单元格间移动;通常使用 Tab 进入/离开复合控件 |
<details> 摘要 | 按 Enter/Space 切换展开状态 |
不要仅凭摘要表实现控件——请使用完整的当前 APG 模式,记录任何差异,并使用有代表性的浏览器和辅助技术进行测试。
关键:对话框焦点管理
不正确的对话框焦点管理属于关键问题——键盘和屏幕阅读器用户会失去当前位置,或完全无法访问对话框控件。
首选方式:使用 showModal() 的原生 <dialog>
使用 showModal() 打开的模态对话框会自动将文档其余部分设为 inert 状态——与手动对每个同级元素应用 inert 相比,这现在是首选模式。
<button type="button" id="delete-trigger">Delete project</button>
<dialog id="delete-dialog" aria-labelledby="delete-title">
<h2 id="delete-title">Delete project?</h2>
<p>This action cannot be undone.</p>
<form method="dialog">
<button value="cancel" autofocus>Cancel</button>
<button value="confirm">Delete project</button>
</form>
</dialog>
const trigger = document.getElementById('delete-trigger');
const dialog = document.getElementById('delete-dialog');
trigger.addEventListener('click', () => dialog.showModal());
dialog.addEventListener('close', () => {
if (trigger.isConnected) trigger.focus();
});
初始焦点取决于对话框内容——简短的确认对话框可以将焦点设在“取消”按钮上;较长的信息对话框可以将焦点设在带有 tabindex="-1" 的标题上,让用户从开头开始阅读。不要不考虑内容,就总是将焦点设在第一个可交互元素上。如果对话框打开期间触发控件被移除,应将焦点返回到工作流中最近的合理位置,而不是文档主体。
不要给容器添加 aria-modal="true" 就认为它会形成模态——ARIA 不会让内容变为 inert、限制焦点、添加键盘处理或恢复焦点。
原生模态行为并不意味着无需测试:初始焦点位置、Tab/Shift+Tab 限制、Escape 和可见的关闭控件、表单提交/取消、焦点恢复、嵌套/堆叠覆盖层、屏幕阅读器播报,以及项目支持矩阵中的浏览器支持情况。
手动方式(原生 <dialog> 不可用时)
inert 属性会阻止用户与打开的对话框外部元素进行任何交互(焦点、点击、辅助技术)。它的浏览器支持良好(基线为 2023 年),并且比手动循环切换可聚焦元素更简单、更可靠:
function openDialog(dialog, trigger) {
document.querySelectorAll('body > *:not(#dialog-container)')
.forEach(el => el.setAttribute('inert', ''));
dialog.removeAttribute('hidden');
dialog.querySelector(focusableSelectors)?.focus();
}
function closeDialog(dialog, trigger) {
document.querySelectorAll('[inert]').forEach(el => el.removeAttribute('inert'));
dialog.setAttribute('hidden', '');
trigger.focus();
}
对于不支持 inert 的环境,请使用经过生产环境测试的 focus-trap 库,而不是手动编写焦点循环逻辑;也可以使用 wicg-inert polyfill。
严重:非模态披露内容和弹出层不得限制焦点
除非交互模式明确要求限制焦点,否则不要在普通披露内容、工具提示、菜单或非模态弹出层中限制焦点。
对于简单的显示/隐藏内容,使用带有 aria-expanded 的按钮即可:
<button type="button" aria-expanded="false" aria-controls="filter-panel">
Filters
</button>
<div id="filter-panel" hidden></div>
当打开的内容覆盖页面时:确保键盘用户可以进入和离开;当交互移开时关闭非持久内容;在用户通常会期待时支持 Escape;如果关闭后会导致焦点丢失,则将焦点移回;防止覆盖层遮挡新的焦点位置;不要仅仅因为触发器获得焦点就打开复杂内容。
符合要求时可以使用 HTML Popover API——仍需测试所选弹出层类型的焦点行为和关闭方式。
严重:焦点可见性
每个可聚焦元素都必须有清晰、持续可见的焦点指示器。 移除焦点轮廓却没有提供同样醒目的替代样式,属于严重问题。
:focus-visible {
outline: 3px solid var(--focus-color, #005fcc);
outline-offset: 3px;
}
@media (forced-colors: active) {
:focus-visible { outline-color: Highlight; }
}
区分适用的要求:
- **2.4.7 焦点可见(AA):**一种可见的操作模式,在该模式下焦点是可见的
- **1.4.11 非文本对比度(AA):**组件状态可能需要与相邻颜色达到 3:1 的对比度
- **2.4.13 焦点外观(AAA):**增强的最小指示器面积,以及焦点与未聚焦像素间 3:1 的变化——不要把这一 AAA 公式描述为 AA 要求
对比度足够的 3px 实线轮廓是实用的项目默认方案,但并不完整满足焦点外观公式。请在浅色、深色、高对比度和强制颜色模式下,以及控件所处的每种背景下测试焦点。
严重:焦点不得被遮挡(WCAG 2.4.11/2.4.12)
固定页眉、固定页脚、Cookie 通知、聊天面板、工具栏、虚拟键盘和持久披露内容都可能遮挡获得焦点的元素,即使焦点在技术上可见也是如此。获得焦点的元素完全隐藏在固定覆盖层后面,属于严重问题。
html {
scroll-padding-block-start: var(--sticky-header-height, 0);
scroll-padding-block-end: var(--sticky-footer-height, 0);
}
[id] {
scroll-margin-block-start: var(--sticky-header-height, 0);
}
CSS 滚动间距有所帮助,但不能证明符合要求——请在每个断点、缩放级别、文本间距配置和持久覆盖层下进行测试。WCAG 2.4.11(AA)要求获得焦点的元素不得被完全隐藏;部分遮挡属于中等问题,完全隐藏属于严重问题。优先采用能让整个获得焦点的组件及其指示器保持可见的设计——这超出了 AA 的最低要求,并符合 2.4.12 增强的焦点不得被遮挡(AAA)。
严重:焦点顺序
Tab 顺序必须遵循合理的阅读和交互顺序。 不合理的焦点顺序属于严重问题——屏幕阅读器用户会根据焦点顺序建立页面的空间模型,焦点顺序错乱会破坏这一模型。
- 以语义化 DOM 顺序为主要机制;使视觉顺序与 DOM 顺序一致——不要使用 CSS
order、网格定位或绝对定位来创建误导性的视觉顺序 - 绝不要使用正数
tabindex值——它们会创建单独且难以维护的焦点顺序 tabindex="0"——仅当元素确实应属于顺序焦点序列,且没有原生元素能提供相应语义时使用tabindex="-1"——仅用于程序化焦点目标,以及 roving-tabindex 模式中的非活动项- 除非有特定的交互要求,否则不要将非交互式标题/段落/容器加入 Tab 顺序
- 除非将初始焦点置于此处符合预期、经过测试且不会令用户意外,否则不要使用
autofocus - 插入、移除、重新排序或筛选内容时,保留焦点或将其移至合理的位置——绝不要让焦点停留在已删除的元素上
严重:复合组件(Roving Tabindex)
复合组件(工具栏、单选按钮组、树视图、标签列表、菜单栏)通常只提供一个 Tab 停靠点;用户通过方向键或其他已说明的按键在内部项目之间移动。两种公认的策略:
- Roving tabindex: 一个项目的
tabindex="0",其他项目为-1;移动时更新这些值并移动 DOM 焦点 aria-activedescendant: DOM 焦点停留在容器上,同时由该属性标识当前活动的后代
选择相关组件模式指定的策略。不要为原生单选按钮组添加自定义 roving tabindex——浏览器已经提供了相应的键盘操作行为。
<div role="toolbar" aria-label="Text formatting">
<button type="button" tabindex="0" aria-pressed="false">Bold</button>
<button type="button" tabindex="-1" aria-pressed="false">Italic</button>
<button type="button" tabindex="-1" aria-pressed="false">Underline</button>
</div>
toolbar.addEventListener('keydown', (event) => {
const items = Array.from(toolbar.querySelectorAll('button:not([disabled])'));
const current = items.indexOf(document.activeElement);
let next = current;
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') next = (current + 1) % items.length;
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') next = (current - 1 + items.length) % items.length;
else if (event.key === 'Home') next = 0;
else if (event.key === 'End') next = items.length - 1;
else return;
event.preventDefault();
items.forEach((item, index) => { item.tabIndex = index === next ? 0 : -1; });
items[next].focus();
});
生产环境中的工具栏还必须处理方向、书写方向、禁用项目和动态添加的项目。请参阅 APG roving tabindex 实践,了解各组件类型对应的具体方向键方向。
中等:字符键快捷键(WCAG 2.1.4)
如果快捷键仅使用可打印字符、标点符号、数字或符号,则至少提供以下一种方式:关闭快捷键、将快捷键重新映射为包含非打印修饰键的组合,或仅在相关组件获得焦点时启用快捷键。语音输入可能会意外触发单字符快捷键。不要拦截浏览器、操作系统或辅助技术的快捷键;除非组件确实提供了可访问的等效行为来替代默认行为,否则应避免阻止默认行为。应以辅助技术能够感知的形式显示快捷键说明。
中等:跳过链接与地标
缺少跳过链接属于中等问题——视力正常的键盘用户每次加载页面时都必须按 Tab 键遍历所有导航。屏幕阅读器用户可以通过地标导航作为替代方式,但跳过链接是绕过这些内容最省力的方式。
<a class="skip-link" href="#main-content">Skip to main content</a>
<header>…</header>
<nav aria-label="Primary">…</nav>
<main id="main-content" tabindex="-1">…</main>
<footer>…</footer>
.skip-link {
position: fixed;
z-index: 1000;
inset-block-start: 0.5rem;
inset-inline-start: 0.5rem;
padding: 0.75rem 1rem;
color: #ffffff;
background: #000000;
transform: translateY(-200%);
}
.skip-link:focus { transform: translateY(0); }
跳过链接必须在获得焦点时可见——永久隐藏(display: none)属于严重问题,会违反 WCAG 2.4.1。使用原生地标,不要添加冗余的 role 属性(使用 <header>,而非 <header role="banner">),除非该元素在当前上下文中未暴露其原生地标角色。为多个导航地标指定不同的可访问名称。
中等:隐藏、不可用和惰性内容
选择与预期状态相符的机制:
| 机制 | 视觉呈现 | 顺序焦点 | 无障碍树 |
|---|---|---|---|
hidden / display: none | 隐藏 | 移除 | 通常会移除 |
visibility: hidden | 隐藏 | 移除 | 通常会移除 |
inert | 通常可见,除非另行设置样式 | 移除后代元素 | 后代元素会被排除 |
aria-hidden="true" | 不变 | 不变 | 对辅助技术隐藏内容 |
绝不要将可聚焦元素放在 aria-hidden="true" 子树中——
aria-hidden 不会阻止键盘焦点,因此可能导致控件获得焦点,却未向屏幕阅读器公开。对于应不可用的已关闭披露内容,请使用 hidden;如果可见内容需要暂时变为不可交互,请使用 inert。使用 showModal() 的原生 <dialog> 可提供文档级模态行为,无需手动对每个同级元素应用 inert。移除 hidden/inert 时,请确认焦点顺序仍然合理。
中等:动态内容与客户端导航
不要仅仅因为内容更新就移动焦点——只有当用户需要新的交互上下文,或否则会失去当前位置时才移动焦点。适用情形包括:打开或关闭模态框、提交失败后将焦点移至错误摘要、客户端路由更改后将焦点移至主标题,以及在当前获得焦点的元素被删除时恢复焦点。路由更改时:更新文档标题,将焦点移至合适的标题或主容器,在不重复播报的前提下播报新的上下文,并保留预期的后退/前进行为。不要使用正数 tabindex 或反复触发合成 Tab 事件来管理应用焦点。
中等:触摸与指针等效操作(WCAG 2.5.x)
| 要求 | 描述 | 严重程度 |
|---|---|---|
| 2.5.1 指针手势 | 多点或基于路径的手势需要提供单指针或键盘替代操作 | 严重 |
| 2.5.3 名称中的标签 | 可见标签文本必须包含在可访问名称中 | 严重 |
| 2.5.8 目标尺寸下限 | 交互目标至少为 24×24 CSS 像素;建议为 44×44 | 中等 |
| user-scalable=no | 切勿禁止双指缩放——这会阻碍低视力用户 | 严重 |
| 拖动以重新排序 | 必须提供键盘替代操作(剪切/粘贴、上/下按钮) | 严重 |
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1">
测试要求
- 页面级: 从浏览器界面按 Tab 键进入页面;按 Tab/Shift+Tab 键双向遍历整个页面;确认每项交互功能都可通过键盘访问和操作;确认非活动/隐藏控件不会获得焦点;确认焦点指示器始终可见;确认粘性/覆盖内容不会完全遮挡焦点;激活跳过链接
- 组件: 测试未覆盖浏览器行为的原生控件;测试每种自定义组件所记录的所有按键;测试首项/末项/禁用项/动态添加或移除的项目;测试按 Tab/Shift+Tab 键进入和退出;在适用时测试书写方向和方向;确认键盘激活效果与指针激活效果一致
- 对话框/覆盖层: 确认初始焦点位置恰当;确认模态框的焦点不会离开对话框;确认可见控件和 Escape 键都能关闭对话框;确认焦点会按逻辑返回;确认非模态内容不会困住焦点;测试嵌套覆盖层以及反复打开/关闭
- 辅助技术和显示: 在运行屏幕阅读器时测试键盘操作;测试 200%/400% 缩放;320px 视口;浅色/深色/增强对比度/强制颜色模式;测试使用可见标签作为命令的控件的语音输入
自动化检查可以检测正值 tabindex、非交互元素上的点击处理程序、aria-hidden/inert 内容中的可聚焦后代、缺失的可访问名称,以及某些焦点指示器问题——但无法判断完整的焦点顺序是否合理,也无法判断自定义交互是否遵循预期的键盘交互模式。每种新增或更改的交互模式都应保留手动键盘检查流程。
完成定义检查清单
- 按 Tab 键遍历整个页面:顺序合理,没有意外跳过
- 每个可聚焦元素都有可见的焦点指示器(浅色和深色模式)
- 所有交互元素都能按照组件类型表中的正确按键激活
- 没有键盘陷阱(有意设置的模态框陷阱除外,且必须能用 Escape 退出)
- 对话框使用原生
<dialog>/showModal()(或使用inert/焦点陷阱作为后备方案);焦点移至第一个合适的元素;关闭时焦点返回触发元素 - 非模态披露内容/弹出层不会困住焦点
- 存在跳过链接,位于 DOM 中首位,并在获得焦点时可见
- 跳过链接目标设有
tabindex="-1",以便通过编程方式聚焦 - 粘性页眉/页脚:使用 scroll-padding/margin 防止聚焦元素被隐藏
- 隐藏内容不在 Tab 顺序中;
aria-hidden内没有可聚焦元素 - 复合组件使用 roving tabindex 或
aria-activedescendant - 字符键快捷键可以禁用、重新映射或限定作用范围
- 拖动交互提供键盘替代操作
- 触摸目标至少达到 24×24px(建议 44×44)
- 不使用
user-scalable=no - 焦点不会被重叠的粘性元素遮挡
关键 WCAG 标准
- 2.1.1 键盘(A)——违反时为严重问题
- 2.1.2 无键盘陷阱(A)——违反时为严重问题
- 2.1.4 字符键快捷方式(A)
- 2.4.1 跳过区块(A)
- 2.4.3 焦点顺序(A)
- 2.4.7 焦点可见(AA)
- 2.4.11 焦点未被遮挡(最低)(AA,WCAG 2.2)
- 2.4.12 焦点未被遮挡(增强)(AAA,WCAG 2.2)
- 2.4.13 焦点外观(AAA)
- 2.5.1 指针手势(A)
- 2.5.3 名称中的标签(A)
- 2.5.8 目标尺寸(最低)(AA,WCAG 2.2)
参考资料
- 完整最佳实践指南
- WAI-ARIA 编写实践指南(APG) — 版本独立于 WCAG
- APG:开发键盘界面
- APG:模态对话框模式
- HTML Living Standard:
dialog元素 - HTML Living Standard:
inert属性 focus-trap库wicg-inertpolyfill
**标准展望:**这些规则以 WCAG 2.2 AA 为目标。WCAG 3.0 仍在 制定中;预计键盘和焦点要求将与 2.2 大体兼容。 关注:https://www.w3.org/TR/wcag-3.0/