关于 anchor-links
anchor-links 是面向网页开发与审查的无障碍技能,适用于包含页内导航或跳过链接的项目。它覆盖链接语义、目标与焦点、减少动态效果及测试要点。
主要功能
- 01检查链接文本与导航语义
- 02验证片段目标唯一且可见
- 03规范跳过链接与焦点管理
- 04兼顾粘性页眉和减少动态效果
- 05提供键盘、读屏与自动化测试指引
适用场景
- 01审查目录链接和标题永久链接
- 02检查跳过链接是否可键盘访问
- 03排查固定页眉遮挡锚点的问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mgifford/ accessibility-skills --skill anchor-links安装后在 Claude Code 中输入/anchor-links确认已加载。
附带文件SYNC.md
详细介绍
锚点链接无障碍技能
规范来源:
mgifford/ACCESSIBILITY.md中的examples/ANCHOR_LINKS_ACCESSIBILITY_BEST_PRACTICES.md此技能源自该文件。如有疑问,以示例为准。
创建或审查页面内锚点链接、跳过链接或标题链接时,请遵循以下规则。 仅当项目包含页面内导航或跳过链接时,才加载此技能。
核心要求
每个锚点链接都必须具有有意义的文本、可到达且带有可见焦点指示器的目标,并且不得造成与运动相关的伤害。使用带 href 的真实 <a> 元素进行导航;仅在执行不涉及导航的操作时使用按钮。添加 JavaScript 之前,优先使用原生片段导航。
严重程度等级(本技能)
| 等级 | 含义 |
|---|---|
| 严重 | 锚点导航阻碍访问或造成困局 |
| 高 | 跳过链接失效或永久隐藏;平滑滚动造成前庭伤害 |
| 中等 | 焦点指示器被固定页眉遮挡;链接文本在上下文中含义不明确 |
| 轻微 | ID 不符合 slug 格式;目录中缺少 aria-current |
导航使用链接,操作使用按钮
激活后会更改 URL 或跳转到某个位置时,使用锚点;激活后执行不涉及导航的操作时,使用按钮。
<a href="#installation">Installation instructions</a>
<button type="button" id="copy-installation-link">Copy link to Installation</button>
请勿使用虚设链接执行操作:
<a href="#">Open settings</a>
<a href="javascript:void(0)">Open settings</a>
空片段会跳转到文档顶部;JavaScript URL 会失去普通链接语义(历史记录、复制、在新标签页中打开、无脚本回退)。不要给带有 href 的原生锚点添加 role="link"——它已经具有该角色。
高:链接文本须有意义
WCAG 2.4.4 要求仅凭链接文本,或链接文本加上程序化确定的上下文,就能判断链接的用途——无需任何上下文即可理解每个链接,是更高的 AAA 级标准(2.4.9),但仍是值得追求的目标。
<a href="#installation">Installation instructions</a>
<a href="#wcag-criteria">Relevant WCAG success criteria</a>
<a href="#section">Click here</a>
<a href="#section">Read more</a>
如果程序化确定的上下文能够说明用途,那么“阅读更多”并不一定违反 A 级标准,但更清晰的文本更便于浏览、翻译和语音操作。如果设计必须保留重复的可见文本,请在链接内添加隐藏的上下文,而不要替换可见文本:
<a href="#keyboard-testing">
Read more<span class="visually-hidden"> about keyboard testing</span>
</a>
不要使用 aria-label 将有用的可见文本替换成不同名称——无障碍名称应包含可见文字(支持语音输入,并符合 WCAG 2.5.3「标签名称」要求)。对于仅含图标的链接,“链接到”是多余的——屏幕阅读器已经会播报其角色:
<a href="#installation" aria-label="Installation section">
<svg aria-hidden="true" focusable="false"></svg>
</a>
用途或目标相同的链接应使用一致的名称;文本相同但目标不同的链接则需要足够的上下文以便区分。
高:片段目标必须唯一、可见且稳定
# 后的片段必须解析到文档中唯一的 id;ID 不得包含 ASCII 空白字符。
<nav aria-label="On this page"><a href="#installation">Installation</a></nav>
<h2 id="installation">Installation</h2>
请勿将以下内容设为目标:带有 hidden/display:none/visibility:hidden 的元素;已关闭的折叠内容中的内容;非活动标签页面板;距离可见标题很远的空标记;或在 hydration 期间被移除的元素。如果深层链接指向折叠内容或标签页内容,请在滚动或移动焦点之前展开容器。
**保持已发布的 ID 稳定:**目标概念相同时,保留现有 ID;必须更改 ID 时,提供别名/重定向方案;不要仅因标题文本或大小写发生变化就重新生成所有 ID。生成 slug 的系统必须以确定的方式处理重复标题,并确保服务器端渲染和客户端 hydration 生成相同的 ID。
严重:跳过链接
必须是第一个可聚焦控件(或首小组跳过链接中的一员),并在获得焦点时完全显示。
永久隐藏的跳过链接(display:none、visibility:hidden)属于严重问题——它完全违背了 WCAG 2.4.1 的要求。
<body>
<a class="skip-link" href="#main-content">Skip to main content</a>
<header></header>
<main id="main-content" class="skip-target" tabindex="-1">
<h1>Page title</h1>
</main>
</body>
.skip-link {
position: absolute;
z-index: 1000;
inset-block-start: 0;
inset-inline-start: 1rem;
padding: 0.75rem 1rem;
color: #ffffff;
background: #111827;
font-weight: 700;
transform: translateY(-150%);
}
.skip-link:focus {
transform: translateY(0);
outline: 3px solid #facc15;
outline-offset: 2px;
}
@media (forced-colors: active) {
.skip-link { color: LinkText; background: Canvas; border: 1px solid LinkText; }
.skip-link:focus { outline-color: Highlight; }
}
激活后:焦点位于主要内容中;主要标题/内容开头可见;按下一次 Tab 会到达主要内容中的第一个逻辑交互元素;不会先遍历被跳过的导航。tabindex="-1" 允许元素获得焦点,但不会添加 Tab 停靠点——它不属于顺序 Tab 键序列。请在 400% 缩放和长篇翻译文本下进行测试;聚焦的链接不能被视口、Cookie 横幅、固定页眉或 overflow: hidden 祖先元素裁切。
中等:普通页内链接的焦点管理
滚动和焦点有关联,但并不相同——视觉上的滚动并不能证明键盘焦点或屏幕阅读器的关注点已移到有用的位置。从原生 HTML 入手,不要取消点击行为:
<a href="#testing">Testing</a>
<h2 id="testing">Testing</h2>
不要仅仅因为 JavaScript 能做到,就自动给每个标题添加 tabindex="-1",或对每个片段调用 .focus()。当原生行为无法提供清晰易懂的过渡,或导航已被拦截时,才适合以编程方式设置焦点:
function focusFragmentTarget(target) {
if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '-1');
target.focus({ preventScroll: true });
target.scrollIntoView({ block: 'start' });
}
不要添加正值 tabindex;不要将焦点设到隐藏元素上。当焦点移至通常不可交互的目标时,应提供明显的方位提示:
.skip-target:focus, .programmatic-focus-target:focus {
outline: 3px solid currentColor;
outline-offset: 0.25rem;
}
中等:粘性和固定页眉
使用 CSS 滚动偏移,避免片段目标和聚焦元素被粘性页眉遮挡(WCAG 2.4.11「焦点未被遮挡(最低)」也适用于此处):
:root { --anchor-offset: 5rem; }
html { scroll-padding-block-start: var(--anchor-offset); }
h2[id], h3[id], main[id], .anchor-target {
scroll-margin-block-start: var(--anchor-offset);
}
在实际发生滚动的元素上设置 scroll-padding-block-start——对于内部滚动区域,只在 html 上设置不会生效。偏移量必须与响应式断点、缩放级别以及横幅的显示/隐藏状态下的页眉高度相匹配。
严重:平滑滚动必须遵循 prefers-reduced-motion
最稳妥的默认做法是使用即时原生滚动。无条件设置 scroll-behavior: smooth 可能引发前庭障碍。
未添加媒体查询保护就全局应用平滑滚动,属于严重问题。
/* Never unconditional */
/* Bad: html { scroll-behavior: smooth; } */
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
const allowMotion = window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
target.scrollIntoView({ behavior: allowMotion ? 'smooth' : 'auto', block: 'start' });
不要将平滑滚动与视差、动画高亮或其他会让目标更难追踪的动态效果搭配使用。不要仅为复现原生片段导航已有的行为而添加 JavaScript。
中等:目录和滚动监测
<nav aria-labelledby="contents-heading">
<h2 id="contents-heading">Contents</h2>
<ol>
<li><a href="#installation">Installation</a></li>
<li><a href="#configuration">Configuration</a></li>
</ol>
</nav>
让链接措辞与目标标题一致;保留标题层级;不要让冗长的目录变成用户还需要绕过的另一大块内容。
对于滚动监测,请用 aria-current="location"(而非 "true")标记当前位于视口中的章节,并通过 IntersectionObserver 或滚动监听器更新:
<nav aria-labelledby="on-this-page-heading">
<ul>
<li><a href="#overview" aria-current="location">Overview</a></li>
<li><a href="#testing">Testing</a></li>
</ul>
</nav>
也要在视觉上呈现当前状态;一次只将一个链接标为当前项。不要通过实时区域播报每次滚动监测状态变化,也不要在每次状态更新时都添加新的历史记录条目。
中等:标题永久链接和返回顶部
永久链接会导航到标题的片段;请让它在视觉上/结构上与标题分开,以免其可访问名称成为标题文本的一部分:
<div class="heading-with-permalink">
<h2 id="installation">Installation</h2>
<a class="permalink" href="#installation">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">…</svg>
<span class="visually-hidden">Permalink to Installation</span>
</a>
</div>
永久链接需要有能标识章节的可访问名称、可见的焦点指示器、实用的目标尺寸;如果图标用于标识链接,还需有足够的对比度,并且不能只在指针悬停时提供该链接。如果激活后是复制 URL 而非导航,请使用名称为“复制指向 Installation 的链接”的按钮,并在复制成功后显示状态消息。
对于返回顶部链接,请使用明确的目标,而不要使用空的 #:
<header id="page-top" class="programmatic-focus-target" tabindex="-1">…</header>
<a href="#page-top">Back to top</a>
确保目标不会被粘性内容遮挡,也不要频繁重复该链接,以免给每个较短的章节增添干扰。
严重:单页应用和路由器
无障碍路由器必须处理路由导航和片段导航:
- 渲染带有可用
href的真实锚点(例如/settings#notifications) — 路由或片段导航绝不要使用按钮 - 保留修饰键单击、在新标签页中打开、复制链接和无脚本行为
- 等待目标路由和目标元素渲染完成;导航到目标元素前,先展开折叠的父级
- 更新 URL 并创建相应的历史记录条目;滚动正确的容器;路由变更需要时移动焦点
- 在初始加载、
hashchange和前进/后退导航时处理片段 - 防止过期的异步路由内容在用户已导航到其他位置后移动焦点
完整的路由变更通常需要管理文档标题和主标题焦点(参见 skills/navigation/SKILL.md);同页片段变更不一定需要相同的路由播报。
次要:URL 和历史记录稳定性
- 激活后保留 URL 中的片段 — 支持收藏、分享、重新加载和前进/后退
- 滚动后不要移除哈希;如果使用
history.replaceState()进行常规锚点导航会导致“后退”失效,就不要使用它 - 使用有意义的 slug ID(
#installation-guide,而不是#section-3)— 这是有文档记录的编写约定,并非 WCAG 要求(HTML 允许使用字母和连字符以外的更多字符) - ID 不得包含空格;同一页面内的 ID 必须唯一
测试
- 键盘(跳过链接): 从浏览器界面按 Tab;确认跳过链接是第一个或接近第一个可聚焦元素;确认焦点指示器显示时链接完全可见;按 Enter 将焦点移至主要内容;继续按 Tab 时,焦点应移至主要内容中的第一个逻辑控件
- 键盘(锚点链接): 定位到链接,确认焦点指示器显示;按 Enter 激活;确认目标可见且未被遮挡;确认 Tab/Shift+Tab 顺序合理;确认前进/后退时历史记录和焦点得到保留
- 屏幕阅读器: 检查链接列表中的名称是否有用;激活跳过链接并确认主要内容成为新位置;激活目录链接并确认目标位置易于理解;测试直接片段 URL 在加载/重新加载时的表现
- 视觉/缩放/动态效果: 测试 200%/400% 缩放;窄视口;确认粘性页眉/横幅不会遮挡目标或焦点;测试文本间距覆盖;高对比度/强制颜色;减少动态效果时应立即滚动;测试 RTL 内容
- 触控/语音: 激活区域大小适当;永久链接不需要悬停即可使用;无障碍名称与可见标签相符,便于语音输入
自动化检查可以发现:没有无障碍名称的链接、空或无效的 href、无法解析的片段、重复 ID、ID 中包含空白字符、正值 tabindex、使用 aria-hidden 隐藏但仍可聚焦的内容,以及没有文本替代的纯图标链接。应针对构建后的输出运行链接检查器,而不只是检查源 Markdown。自动化无法确认焦点位置、粘性页眉的遮挡情况、动态效果表现,或目标位置是否易于理解。
常见故障
| 故障 | 修正方法 |
|---|---|
| 使用按钮进行片段导航 | 使用带有真实 href 的锚点 |
使用 href="#" 触发 JS 操作 | 使用按钮,或指向真实的片段目标 |
用不同的 aria-label 替换可见文本 | 在可访问名称中保留可见文字 |
| 链接到缺失或重复的 ID | 创建一个唯一且匹配的目标 |
| 每当标题文本变化时重新生成 ID | 保留已发布的深层链接 |
| 自动将焦点移到每个标题 | 使用原生导航;仅在需要时添加焦点管理 |
给标题添加 tabindex="0" | 仅在需要有意进行程序化聚焦时使用 tabindex="-1" |
| 滚动到被粘性页眉遮挡的内容 | 使用正确的滚动内边距/外边距;测试响应式偏移 |
| 无条件启用平滑滚动 | 使用 prefers-reduced-motion: no-preference 进行限制 |
| 每次滚动监听更新时都推送历史记录条目 | 更新当前状态,但不要污染历史记录 |
| 链接到已关闭或未激活的内容 | 导航前展开包含该内容的披露区/面板 |
完成标准检查清单
- 所有链接文本都清晰明确,即使脱离上下文也有意义
-
aria-label中不使用“链接到”——角色已会被读出 - 目标元素具有唯一的
id值;已发布的 ID 保持稳定 - 仅在需要程序化聚焦时,才为非交互目标设置
tabindex="-1" - 跳过链接:位于 DOM 的首位(或接近首位),聚焦时可见,绝不使用
display:none/visibility:hidden -
scroll-padding/scroll-margin可避免粘性页眉遮挡目标和焦点 - 平滑滚动仅在
prefers-reduced-motion: no-preference条件下启用 - 在当前活动的目录/滚动监听链接上使用
aria-current="location"(而非"true") - 标题永久链接和返回顶部链接具有可识别的名称和可见目标
- SPA 路由器保留真实的
href、历史记录和修饰键点击行为 - 页面内 ID 唯一,并使用连字符而非空格
- 锚点导航后没有焦点陷阱
关键 WCAG 标准
- 2.4.1 绕过区块(A)——跳过链接失效或被隐藏时属于严重问题
- 2.4.4 上下文中的链接目的(A)——链接文本含糊不清时属于严重问题
- 2.4.7 焦点可见(AA)
- 2.4.9 仅通过链接文本判断链接目的(AAA)——超越 A 级最低要求的优秀设计目标
- 2.4.11 焦点未被遮挡(最低要求)(AA,WCAG 2.2)——粘性页眉遮挡目标
- 2.5.3 标签与名称一致(A)
- 3.2.4 一致的标识(AA)
平滑滚动动画是符合 prefers-reduced-motion 的最佳实践,
但并非 WCAG 2.2 AA 的直接测试项目(2.3.3 交互触发的动画
属于 AAA 级)。不过,作为渐进增强的基线,仍必须设置相应的保护条件。
参考资料
- 完整最佳实践指南
- WCAG 2.2 理解 2.4.4 链接目的
- WCAG 2.2 理解 2.4.9 仅通过链接文本判断链接目的
- WCAG 2.2 理解 2.4.11 焦点未被遮挡
- MDN:prefers-reduced-motion
**标准展望:**这些规则针对 WCAG 2.2 AA。预计 WCAG 3.0 不会对锚点链接模式 引入破坏性变更。 关注:https://www.w3.org/TR/wcag-3.0/