mcpField
N
navigation

navigation

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

关于 navigation

navigation 是用于创建或审查网站导航组件的无障碍技能,适用于设计和开发人员。它涵盖键盘操作、屏幕阅读器地标、下拉菜单、面包屑、分页及移动导航等模式。

主要功能

  1. 01指导导航地标与跳过链接
  2. 02规范当前页面状态标记
  3. 03提供下拉导航键盘交互规则
  4. 04涵盖面包屑与分页模式
  5. 05检查移动导航、缩放和重排

适用场景

  1. 01审查网站主导航的无障碍性
  2. 02实现可键盘操作的下拉菜单
  3. 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浏览器关键行为
NVDAChrome使用 H 键浏览标题,使用 R 键浏览区域;进入导航时会播报列表项数量
JAWSChrome使用 F6 键浏览框架/区域,使用 Q 键移至主要内容;会播报导航地标
VoiceOverSafari(macOS)使用 VO+U 转子浏览地标和链接;会播报导航标签
VoiceOverSafari(iOS)通过滑动浏览;可通过转子访问导航地标
TalkBackChrome(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 时属严重问题

参考资料

标准展望: 这些规则以 WCAG 2.2 AA 为目标。预计 WCAG 3.0 不会对导航要求作出破坏性更改。 请关注:https://www.w3.org/TR/wcag-3.0/

评论

支持 Markdown