关于 navigation
navigation 是用于创建或审查网站导航组件的无障碍技能,适用于设计和开发人员。它涵盖键盘操作、屏幕阅读器地标、下拉菜单、面包屑、分页及移动导航等模式。
主要功能
- 01指导导航地标与跳过链接
- 02规范当前页面状态标记
- 03提供下拉导航键盘交互规则
- 04涵盖面包屑与分页模式
- 05检查移动导航、缩放和重排
适用场景
- 01审查网站主导航的无障碍性
- 02实现可键盘操作的下拉菜单
- 03检查移动菜单与面包屑导航
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mgifford/ accessibility-skills --skill navigation安装后在 Claude Code 中输入/navigation确认已加载。
附带文件README.mdSYNC.md
详细介绍
导航无障碍技能
规范来源:
mgifford/ACCESSIBILITY.md中的examples/NAVIGATION_ACCESSIBILITY_BEST_PRACTICES.md此技能源自该文件。如有疑问,以示例为准。
创建或审查导航模式时,请遵循以下规则:主导航、下拉菜单、面包屑、分页、移动端菜单和页内导航。 仅当项目包含导航组件时才加载此技能。
核心要求
导航必须能够通过键盘、触控、语音输入、屏幕阅读器、屏幕放大、文本缩放和浏览器缩放来理解和操作。屏幕阅读器用户依靠地标结构来确定方位;键盘用户依靠可预测的 Tab 顺序和下拉菜单行为;语音控制用户依靠可见且标签准确的交互元素;放大用户依靠能够重排而不出错的导航。导航设计不当,会让后续所有操作都更困难。
严重性等级(本技能)
| 等级 | 含义 |
|---|---|
| 严重 | 导航完全无法访问,或造成键盘陷阱 |
| 较高 | 下拉菜单只能通过悬停操作;误用 role="menu";缺少 aria-current |
| 中等 | 次级导航缺少 aria-label;面包屑未标注 |
| 轻微 | 导航项超过 7 个;aria-expanded 更新不一致 |
不要仅根据模式本身判定严重性,而应确定其对导航和任务完成的实际影响。考虑用户能否访问主要内容、能否打开、操作、关闭和离开菜单、焦点是否会被困住或丢失,以及缺陷是否会通过共享组件影响每个页面(共享导航陷阱可能会阻挡整个网站)。
辅助技术环境
不同辅助技术(AT)中的导航行为各不相同。请使用以下配置进行测试:
| AT | 浏览器 | 关键行为 |
|---|---|---|
| NVDA | Chrome | 使用 H 键浏览标题,使用 R 键浏览区域;进入导航时会播报列表项数量 |
| JAWS | Chrome | 使用 F6 键浏览框架/区域,使用 Q 键移至主要内容;会播报导航地标 |
| VoiceOver | Safari(macOS) | 使用 VO+U 转子浏览地标和链接;会播报导航标签 |
| VoiceOver | Safari(iOS) | 通过滑动浏览;可通过转子访问导航地标 |
| TalkBack | Chrome(Android) | 通过线性滑动浏览;通过本地上下文菜单访问地标 |
| 语音控制 | 任意 | 用户根据可见链接文本进行导航;所有交互元素都需要准确的可见标签。与可见文本不同的 aria-label 会导致语音控制失效 |
| 屏幕放大 | 任意 | 高倍缩放(200%、400%)时,粘性/固定导航栏会缩小可见视口 |
| 阅读模式 | Firefox/Edge/Safari | 会从文章视图中移除导航——可以接受;主要内容必须在没有导航的情况下仍然易于理解 |
语音控制提示: Dragon 和 iOS Voice Control 通过朗读可见链接文本进行导航。如果 aria-label 与可见文本不同,用户就无法通过朗读所见内容来激活链接——可访问名称必须包含可见文本(WCAG 2.5.3 名称中的标签)。
严重:地标结构
<a class="skip-link" href="#main-content">Skip to main content</a>
<header>
<a href="/">Service name</a>
<nav aria-label="Primary">
<ul>
<li><a href="/services">Services</a></li>
<li><a href="/guidance">Guidance</a></li>
</ul>
</nav>
</header>
<main id="main-content" tabindex="-1">
<h1>Page title</h1>
</main>
<footer>
<nav aria-label="Footer">…</nav>
</footer>
<nav>用于包裹帮助用户浏览网站、页面或流程的每个主要链接组;不要将每个偶然出现的链接列表都包在导航地标中,过多的地标会增加区域导航的难度- 存在多个
<nav>元素时,每个元素都需要一个易于理解的名称,以便用户区分它们:aria-label="Primary"、"Footer"、"On this page"——不要包含角色名称(“主导航地标”是多余的,因为角色已被明确告知) - 如果已有可见标题为该区域命名,请使用
aria-labelledby - 对包含相同链接的重复导航地标使用相同名称;地标用途不同时使用不同名称
- 不要向
<nav>添加冗余的role="navigation"
缺少 <nav> 地标属于较高严重性问题——屏幕阅读器用户无法通过转子或地标列表跳转到导航。
严重:跳过链接
必须位于正文开头或附近,且在被跳过的内容之前,
并且必须是最先可用的键盘焦点停靠点之一(不一定是
DOM 树中第一个节点)。永久隐藏的跳过链接(display:none、
visibility:hidden 或 hidden)属于严重问题——这会违背 WCAG 2.4.1。
.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); }
确保激活后视口和键盘焦点都移至目标位置;
在 <main> 上使用 tabindex="-1",以可靠地通过程序设置焦点;确保粘性
页眉不会遮挡目标位置。根据符合性方法,WCAG 也允许使用其他符合要求的
绕过机制(标题、地标)。
严重:当前位置(aria-current)
<nav aria-label="Primary">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/guidance" aria-current="page">Guidance</a></li>
</ul>
</nav>
使用与关系相符的值——并非总是 "true":
aria-current="page"— 当前页面aria-current="step"— 流程中的当前步骤aria-current="location"— 环境中的当前位置(例如,滚动监听目录)aria-current="date"/"time"— 适用时aria-current="true"— 仅在没有更具体的标记适用时使用
不要在普通导航链接上使用 aria-selected——应将其保留给选中状态属于其角色一部分的
控件(标签页、列表框)。还必须提供不单纯依赖颜色的视觉当前项指示器。
当前位置指示器缺少 aria-current 属于严重问题——没有它,屏幕阅读器用户
无法判断自己当前所在的位置。
严重:网站导航不是应用菜单
在普通网站导航上使用 role="menu"/"menuitem"/"menubar" 属于严重问题。
这些角色会使屏幕阅读器进入应用菜单交互模式(用方向键导航,按 Tab 会退出整个菜单)——
这与用户对网页导航的预期不符。
<nav><ul role="menu"><li role="menuitem"><a href="/about">About</a></li></ul></nav>
<nav aria-label="Primary">
<ul><li><a href="/about">About</a></li></ul>
</nav>
两者的区别取决于行为和用途,而非视觉外观。
role="menu"/"menuitem" 仅适用于真正实现完整方向键交互的应用工具栏/操作/
上下文菜单——有关这一特定用例,请参阅
WAI-ARIA APG 菜单栏模式。
严重:披露式(下拉)导航
对于显示/隐藏嵌套链接列表的网站导航,请使用披露按钮——不要强行 将普通导航套用到 ARIA 菜单栏模式中。
<nav aria-label="Primary" data-disclosure-nav>
<ul>
<li data-disclosure-item>
<a href="/services">Services</a>
<button type="button" aria-expanded="false" aria-controls="services-links">
<span class="visually-hidden">Show Services submenu</span>
<svg aria-hidden="true" focusable="false" viewBox="0 0 20 20">
<path d="m5 7 5 5 5-5" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<ul id="services-links" hidden>
<li><a href="/services/design">Design</a></li>
</ul>
</li>
</ul>
</nav>
不要在同一个元素上同时设置链接和下拉触发功能——如果“服务”既是链接又是下拉菜单触发器, 键盘用户就无法在不离开页面的情况下打开下拉菜单。请使用单独的按钮(如上所示),或使用 仅包含按钮的父项,并将“概览”链接作为第一个子项。
document.querySelectorAll('[data-disclosure-nav]').forEach((navigation) => {
const buttons = Array.from(navigation.querySelectorAll('[data-disclosure-item] > button[aria-controls]'));
function close(button, restoreFocus = false) {
const panel = document.getElementById(button.getAttribute('aria-controls'));
button.setAttribute('aria-expanded', 'false');
panel.hidden = true;
if (restoreFocus) button.focus();
}
function open(button) {
const panel = document.getElementById(button.getAttribute('aria-controls'));
button.setAttribute('aria-expanded', 'true');
panel.hidden = false;
}
buttons.forEach((button) => {
button.addEventListener('click', () => {
const expanded = button.getAttribute('aria-expanded') === 'true';
expanded ? close(button) : open(button);
});
const item = button.closest('[data-disclosure-item]');
item.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && button.getAttribute('aria-expanded') === 'true') {
e.preventDefault();
close(button, true);
}
});
item.addEventListener('focusout', (e) => {
if (!item.contains(e.relatedTarget)) close(button);
});
});
document.addEventListener('pointerdown', (event) => {
buttons.forEach((button) => {
const item = button.closest('[data-disclosure-item]');
if (!item.contains(event.target)) close(button);
});
});
});
要求:aria-expanded 和 hidden 保持同步;Tab/Shift+Tab
遵循普通顺序;按 Escape 关闭菜单并将焦点返回按钮;焦点移出父项时关闭其子菜单;打开菜单时
不自动移动焦点或切换页面;将 JavaScript 限定在导航组件内,而不是页面上所有带有
aria-controls 的元素。
悬停行为是可选的——点击/激活必须足够。如果悬停或焦点也会显示内容,WCAG 1.4.13 要求
该内容可关闭(无需移动焦点/指针)、可悬停(指针可以移到内容上)且
持久显示(直到悬停/焦点移除或内容被关闭)。单独使用 CSS
transition-delay 无法满足可悬停/持久显示行为的要求。
中等:面包屑
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/services">Services</a></li>
<li aria-current="page">Web accessibility</li>
</ol>
</nav>
使用 <ol>——顺序具有意义。将装饰性分隔符对辅助技术(AT)隐藏
(优先使用 CSS)。当前项可以是纯文本或链接——不要创建不提供任何有效导航功能的链接。
确保可见的面包屑与实际页面层级一致。WCAG 不要求每个网站都提供面包屑。
中等:分页
<nav aria-label="Pagination">
<ul>
<li><a href="?page=1" rel="prev">Previous</a></li>
<li><a href="?page=1" aria-label="Page 1">1</a></li>
<li><a href="?page=2" aria-label="Page 2" aria-current="page">2</a></li>
<li><a href="?page=3" rel="next">Next</a></li>
</ul>
</nav>
空间允许时,使用清晰可见的“上一页”/“下一页”文本;如果只显示箭头,可访问名称必须包含“上一页”/“下一页”。确保当前页有明显且不依赖颜色的区分。不可用的“上一页”/“下一页”操作不要创建链接。分页 URL 中保留筛选条件、搜索词和排序顺序。
中等:响应式(移动端)导航
除非设计确实需要模态导航抽屉,否则应将典型的响应式菜单视为非模态披露控件——普通汉堡菜单不应对页面其余部分应用 inert。
<button type="button" id="navigation-toggle" aria-expanded="false" aria-controls="responsive-navigation">
<span>Menu</span>
<svg aria-hidden="true" focusable="false"></svg>
</button>
<nav id="responsive-navigation" aria-label="Primary" hidden>
<ul><li><a href="/">Home</a></li></ul>
</nav>
const toggle = document.getElementById('navigation-toggle');
const navigation = document.getElementById('responsive-navigation');
function setNavigationOpen(open) {
toggle.setAttribute('aria-expanded', String(open));
navigation.hidden = !open;
}
toggle.addEventListener('click', () => setNavigationOpen(toggle.getAttribute('aria-expanded') !== 'true'));
navigation.addEventListener('keydown', (event) => {
if (event.key === 'Escape') { setNavigationOpen(false); toggle.focus(); }
});
对于非模态披露控件:在 DOM 顺序中将切换按钮紧挨着放在导航之前;不要将页面其余部分设为 inert;不要陷阱式地限制焦点;不要强制将焦点移到第一个链接(按 Tab 键时,焦点会自然地从切换按钮移至展开的导航);支持按 Escape 键关闭并还原焦点;确保导航之后的链接仍可访问。
如果导航确实是模态抽屉,则应将其实现为完整的模态对话框:限制焦点范围、提供可访问名称、显示关闭按钮、支持 Escape 键、使背景不可交互,并还原焦点。不要仅应用 inert 来实现不完整的模态交互(参见 skills/keyboard/SKILL.md)。
中等:导航一致性与查找页面的多种方式
一组页面中重复出现的导航必须保持相同的相对顺序,除非用户主动更改(WCAG 3.2.3)。保持重复链接的名称和目标一致;除非提供由用户明确控制的个性化功能,否则不要根据推断的用户特征重新排列导航。
对于同一组中的页面,除非页面是流程中的步骤或结果,否则应提供多种查找页面的方式(WCAG 2.4.5),例如层级导航、站内搜索、网站地图、目录或索引。以两种形式呈现同一列表不算作实质上不同的查找方式。
中等:客户端路由与动态导航
路由更改必须建立清晰的新页面上下文:更新文档标题,更新主标题/当前位置状态,将焦点移至合理的目标,保留前进/后退功能,并避免重复播报。
<main id="main-content">
<h1 id="page-heading" tabindex="-1">Account settings</h1>
</main>
document.title = 'Account settings · Service name';
document.getElementById('page-heading').focus();
不要自动同时组合标题焦点、主区域焦点、强提示实时区域和路由播报——这会造成重复或被打断的输出。选择一种经过测试的策略。路由更改链接应保留真正的 <a> 语义——不要仅仅因为路由由 JavaScript 处理,就用按钮代替导航链接。
重排、缩放与放大
在 200%/400% 缩放和窄视口下:导航必须重排,避免二维滚动;控件、标签和焦点指示器不得被裁切;粘性导航不得占用视口中不合理的空间,也不得遮挡获得焦点的组件;用户必须能够关闭持续显示的叠加层;触控目标必须保持可用且互不重叠。请使用浏览器实际缩放进行测试,而不仅仅是调整窗口大小,并在两种屏幕方向下进行测试。
CMS 和框架说明
Drupal: Menu 模块会生成 <nav> 标记;活动路径上的
.is-active 类本身不会添加 aria-current="page" ——
还需通过编程方式设置。请参阅
Drupal 无障碍编码标准。
WordPress: 区块主题使用 <nav> 地标,但当存在多个菜单时,
wp_nav_menu() 默认不会添加 aria-label ——
请使用 nav_menu_args 过滤器。
React/SPA 框架: 导航后必须播报页面变化(见上文的 客户端路由)。如果不这样做,屏幕阅读器用户在路由变化后将听不到任何内容。
检查任何由 CMS/框架生成的导航时,请确认:使用原生链接和
地标;为 disclosure 关系使用稳定的 ID;正确添加/更新
aria-current;可见名称与无障碍名称保持一致;站点导航未添加菜单角色;
aria-expanded/hidden 保持同步;隐藏的响应式变体已从焦点顺序和无障碍树中移除。
优先修正共享模板,而不是单独修补各个页面。
测试
- 结构: 检查地标列表,确认主要区域易于区分; 确认附带的链接组不会产生不必要的地标;确认重复出现的相同导航使用一致的名称; 确认当前项可见且已通过编程方式标识
- 键盘: 激活跳过链接并确认焦点/视口移动; 使用 Tab/Shift+Tab 遍历每个导航控件;使用 Enter/Space 打开/关闭每个 disclosure; 使用 Escape 关闭已打开的子菜单和响应式导航;确认焦点不会被困住、丢失或遮挡; 确认站点导航不需要使用应用菜单的方向键
- 指针/触控/语音: 使用鼠标、触控和大指针进行操作; 确认由悬停触发的内容可关闭、可悬停或可持续显示;确认可见标签包含在无障碍名称中; 通过说出可见标签来激活控件
- 重排: 200%/400% 缩放;320px 视口;纵向/横向; 文本间距覆盖设置;浅色/深色/强制颜色模式
- 路由/韧性: 在需要渐进增强的情况下,关闭 JS 后跟随链接; 测试后退/前进/刷新/深层链接/复制的 URL;确认路由变化会更新标题/标题文本/焦点/当前状态,且不会重复播报; 测试较长标签、本地化和 RTL 文本
自动化检查可以发现重复 ID、失效的 aria-controls
引用、缺失/重复的地标名称、无效的菜单角色、
aria-expanded/hidden 不同步,以及隐藏/惰性导航中的可聚焦内容——
但无法判断标签是否易懂、链接用途是否明确,或完整的导航模型是否可用。请保留手动
键盘、屏幕阅读器、放大和触控测试。
完成标准检查清单
-
<nav>地标包含每个主要导航区域(不包括偶然出现的链接列表) - 每个
<nav>都有唯一且易于理解的名称(名称中不包含角色词) - 跳过链接:位于
<body>开头附近,聚焦时可见,目标具有tabindex="-1" -
aria-current使用与关系相符的值(page/step/location/true) - 网站导航中不使用
role="menu"/"menuitem"/"menubar" - 下拉菜单使用 Disclosure 模式:
aria-expanded、aria-controls、hidden - 顶层链接和下拉菜单触发器是独立元素
- 按下 Escape 可关闭下拉菜单/响应式导航,并将焦点返回触发器
- 悬停显示的内容可关闭、可悬停且会持续显示(WCAG 1.4.13)
- 面包屑:使用
<ol>、命名地标,并在当前项上使用aria-current="page" - 分页:使用命名地标,Previous/Next 名称易于访问,并在当前页面上使用
aria-current - 响应式导航采用完整的非模态模式或完整的模态模式
— 不采用带有部分
inert的混合模式 - 跨页面的重复导航保持一致的相对顺序
- 在 WCAG 2.4.5 适用的情况下,提供多种查找页面的方式
- SPA/框架:路由更改后移动焦点并播报页面标题, 且不重复播报
- 已测试语音控制:通过朗读可见文本即可激活所有链接
- 已在 200%/400% 缩放下测试:导航不会遮挡内容
- 已测试:NVDA+Chrome、JAWS+Chrome、VoiceOver+Safari
关键 WCAG 标准
- 2.4.1 绕过区块(A)— 缺少跳过链接或链接失效时属严重问题
- 2.4.3 焦点顺序(A)
- 2.4.4 上下文中的链接目的(A)
- 2.4.5 多种方式(AA)
- 2.4.7 焦点可见(AA)
- 2.4.11 焦点未被遮挡(最低要求)(AA,WCAG 2.2)
- 1.4.13 悬停或聚焦时显示内容(AA)
- 2.5.3 名称中的标签(A)— 违反时会对语音控制造成严重影响
- 3.2.3 一致的导航(AA)
- 3.2.4 一致的标识(AA)
- 4.1.2 名称、角色、值(A)— 未更新
aria-expanded时属严重问题
参考资料
- 完整最佳实践指南
- 精选资源 — 导航
- WAI 菜单教程
- WAI-ARIA APG — Disclosure 导航
- WAI-ARIA APG — Menubar — 仅用于应用程序工具栏菜单
- Adrian Roselli — 谨慎使用 menu
- Drupal 无障碍编码标准
- WCAG 2.2 理解 3.2.3 一致的导航
- WCAG 2.2 理解 2.4.5 多种方式
标准展望: 这些规则以 WCAG 2.2 AA 为目标。预计 WCAG 3.0 不会对导航要求作出破坏性更改。 请关注:https://www.w3.org/TR/wcag-3.0/