mcpField
K
keyboard

keyboard

github.com/mgifford设计与 UI57207 次安装6 周前更新
代码仓库

关于 keyboard

keyboard 是面向 UI 实现与审查的键盘可访问性技能,涵盖交互控件、焦点管理和复合组件的操作规则。它强调原生控件、符合预期的按键行为及实际测试。

主要功能

  1. 01键盘可达与操作要求
  2. 02常见控件按键规范
  3. 03对话框焦点管理
  4. 04焦点可见性与顺序
  5. 05复合组件键盘交互

适用场景

  1. 01检查交互控件能否仅用键盘操作
  2. 02审查模态对话框的焦点行为
  3. 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 停靠点;用户通过方向键或其他已说明的按键在内部项目之间移动。两种公认的策略:

  1. Roving tabindex: 一个项目的 tabindex="0",其他项目为 -1;移动时更新这些值并移动 DOM 焦点
  2. 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)

参考资料

**标准展望:**这些规则以 WCAG 2.2 AA 为目标。WCAG 3.0 仍在 制定中;预计键盘和焦点要求将与 2.2 大体兼容。 关注:https://www.w3.org/TR/wcag-3.0/

评论

支持 Markdown