mcpField
F
frontend-a11y

frontend-a11y

github.com/mikemai2awesome设计与 UI1684 次安装13 周前更新
代码仓库

关于 frontend-a11y

frontend-a11y 是面向前端开发的无障碍编码指南,适用于原生 HTML 及多种框架。它强调优先使用原生语义和浏览器行为,并提供组件、ARIA 与 CSS 的实践建议。

主要功能

  1. 01优先使用原生 HTML 语义
  2. 02提供多框架适配指导
  3. 03说明地标、控件与表单错误模式
  4. 04指导对话框、导航和轮播设计
  5. 05涵盖无障碍相关 CSS 建议

适用场景

  1. 01为现有前端项目编写无障碍组件
  2. 02检查页面地标和控件的语义
  3. 03设计可键盘操作的导航与对话框
  4. 04改善布局对缩放及书写方向的适应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mikemai2awesome/agent-skills --skill frontend-a11y

安装后在 Claude Code 中输入/frontend-a11y确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
angular.md
aria.md
css.md
patterns.md
react.md
standards.md
svelte.md
vue.md
wcag.json
web-components.md

详细介绍

前端无障碍

尽可能少写代码。优先使用原生 HTML 元素,而不是 ARIA 属性或 JavaScript 库——浏览器已经内置了无障碍支持,而且少写一个属性,就少一处出错的可能。


适配项目的技术栈

这里的所有规则都与框架无关。框架最终都会编译为 HTML,因此无论源代码是 JSX、Vue 模板、Svelte、Angular 还是原始 HTML,<button> 与 <div role="button"> 的区别、对比度比例、WCAG 标准和地标规则都完全相同。下面的 HTML/CSS 示例是权威依据——概念不会改变,只有语法不同。

因此,不要重写此文件——编写代码前稍作停顿,让输出适配项目:

  1. 识别技术栈。 检查 package.json、文件扩展名(.jsx/.tsx、.vue、.svelte)和框架配置。不确定时,参照仓库中现有组件的写法。
  2. 转换示例。 按照项目的惯用写法重新表达每个代码片段——包括属性名称(在 React 中将 class 改为 className、将 for 改为 htmlFor)、事件绑定和 ID 生成方式——同时保持语义一致。
  3. 留意陷阱。 框架会带来原始 HTML 中没有的风险:永远不会移动焦点的客户端路由、可能导致 XSS 的模板逃生通道,以及掩盖原生语义的组件库。如果项目技术栈与下方某个参考文件匹配,请先阅读该文件——它只介绍相对于 HTML 基线的差异。
技术栈阅读
React / Next.js / Preactreact.md
Vue / Nuxtvue.md
Svelte / SvelteKitsvelte.md
Angularangular.md
Web 组件 / Lit / Stencilweb-components.md

如果使用原始 HTML,或技术栈未列在此处,直接按下方示例操作即可。


HTML 指南

使用原生元素

浏览器已经提供了无障碍元素。直接使用它们。


<div role="button" tabindex="0" onclick="submit()">Submit</div>


<button type="submit">Submit</button>
不要添加冗余角色

地标元素已经有隐式角色。不要重复声明。


<header role="banner">...</header>
<nav role="navigation">...</nav>
<main role="main">...</main>
<footer role="contentinfo">...</footer>


<header>...</header>
<nav>...</nav>
<main>...</main>
<footer>...</footer>
不要重复命名地标

屏幕阅读器会按角色播报地标——“导航”“横幅”“主要内容”“内容信息”。在可访问名称中重复该词会导致播报重复(“导航、导航”)。名称应当用于_区分_同类地标,而不是重复说明其角色。

  • <nav> 的 aria-label 不得包含“导航”。应根据_导航的内容_命名,例如“主导航”“页脚”“面包屑”,这样屏幕阅读器会播报“主导航、导航”。
  • <header>(banner)、<main> 和 <footer>(contentinfo)不需要 aria-label。每个页面各只有一个,因此无需与同级元素区分。只有存在多个相同角色的地标时(例如两个 <nav>),才需要命名。

<nav aria-label="Main navigation">...</nav>
<header aria-label="Header">...</header>
<main aria-label="Main content">...</main>


<nav aria-label="Primary">...</nav>
<nav aria-label="Footer">...</nav>
<header>...</header>
<main>...</main>
仅在具有实际标题时才将区域命名

只有在 <section> 具有可访问名称时,它才会成为地标(隐式 role="region"),并出现在屏幕阅读器的地标菜单中。地标是导航快捷方式,因此只有在标记了确实独立且有名称的区域时才有帮助。泛泛命名的区域(“卡片”“更新”)只会_制造噪音_,让菜单变得杂乱。

  • 仅当内容块显示实际可见的标题时,才将其设为区域,并使用指向该标题 id 的 aria-labelledby 为其命名。这样,可访问名称_就是_屏幕上的文本——只有一个信息源,文案变更时也会保持同步。
  • 切勿为了满足区域要求而凭空添加合成的 aria-label。没有标题?使用普通的 <div> 包装元素——样式相同,也不会成为地标。

<section aria-label="Article cards">...</section>


<section aria-labelledby="latest-heading">
  <h2 id="latest-heading">Latest articles</h2>
  ...
</section>


<div class="cards">...</div>
使用语义元素,而非 Div

用有实际含义的元素替换通用容器。


<div class="header">
  <div class="nav">...</div>
</div>


<header>
  <nav>...</nav>
</header>
设置按钮类型

在表单中,未设置 type 的 <button> 默认类型为 submit,点击后会提交表单。务必显式设置 type。


<button onclick="toggle()">Toggle</button>


<button type="button" onclick="toggle()">Toggle</button>
<button type="submit">Submit</button>
永远不要禁用按钮——使用 aria-disabled

原生 disabled 属性会将 <button> 同时从 Tab 键顺序和无障碍树中移除,因此键盘用户和屏幕阅读器用户无法聚焦按钮,既不知道它存在,也无法了解它_为何_不可用——这是本可避免的退步。

  • 需要非活动状态的非提交控件 → aria-disabled="true"。 控件仍可聚焦,会被播报为“灰显”/“不可用”,并且你需要在 JavaScript 中阻止其操作。典型情况:轮播箭头等导航控件,用户仍需_找到_控件并感知其处于非活动状态,而不是看着它消失。
  • 提交按钮 → 始终保持活动状态,既不使用 disabled,也不使用 aria-disabled。让点击触发验证并播报错误,而不是显示一个不会说明缺少什么的失效按钮。
  • 表单_字段_(<input>、<select>、<textarea>)→ 使用原生 disabled 是正确的。 在此处,它决定是否提交字段值——这与控件交互性是不同的问题。

<button type="submit">Submit</button>


<button
  type="button"
  aria-label="Scroll to next"
  aria-disabled="true"
  onclick="scrollNext()"
>
  <svg aria-hidden="true">...</svg>
</button>
切换状态时,保持按钮名称不变

带有 aria-expanded 的切换按钮已经会播报其状态:“菜单,按钮,已折叠” → “菜单,按钮,已展开”。按钮的_名称_应描述它控制的内容,并在两种状态下保持不变。将名称切换为“打开菜单”/“关闭菜单”,会把状态重复放进名称中,甚至可能与播报的状态相矛盾。


<button aria-expanded="false" aria-label="Open menu">...</button>


<button aria-expanded="false" aria-controls="menu" aria-label="Menu">
  ...
</button>
纯图标控件需要无障碍名称

仅包含图标的控件没有可供屏幕阅读器播报的文本。请使用 aria-label 为其命名,并将装饰性图标从无障碍树中隐藏。

<button type="button" aria-label="Close">
  <svg aria-hidden="true">...</svg>
</button>
按内容而非类型描述图像

屏幕阅读器已经会将 <img> 播报为“图像”(或“图形”),因此 alt 应描述_内容_,而非类别。在 alt 中使用“图像”“照片”“图形”“图标”或“徽标”等词,只是在重复其角色。

  • 品牌徽标的 alt 应为品牌名称,而不是“徽标”。如果装饰性徽标没有提供超出相邻文本的信息,则设置 alt="",使其跳过播报。
  • 描述内容图像所呈现的主体。

<img src="logo.svg" alt="Acme logo" />
<img src="team.jpg" alt="photo of a family" />


<img src="logo.svg" alt="Acme" />
<img src="team.jpg" alt="Family reviewing plan options at a table" />
链接图像的 alt 就是链接名称

当 <img> 是链接唯一的内容(例如链接到首页的徽标、卡片缩略图)时,图像的 alt_就是_链接的无障碍名称。包裹它的 <a> 上的 aria-label 会覆盖该名称,并破坏图像的语义——图片实际上就消失了。通过 alt 描述链接的目标,并让链接保持未标记状态。只有在确实没有图像可用于命名时,才将 aria-label 作为_备用方案_添加到链接上。


<a href="/" aria-label="Home page"><img src="logo.svg" alt="" /></a>


<a href="/"><img src="logo.svg" alt="Acme" /></a>


<a href="/"><span class="brand">Acme</span></a>
视觉隐藏文本用于命名,而非提示

使用 visuallyhidden 实用类(有时称为 sr-only)为元素提供仅供屏幕阅读器使用的无障碍名称——例如,只有徽标可见标题的面板仍需要一个标题来命名。但交互控件_内部_的 visuallyhidden 文本会成为该控件名称的一部分,因此不适合用作补充_提示_(例如“在新标签页中打开”)——请参阅下一节。

<span class="visuallyhidden">Main navigation</span>
.visuallyhidden:not(:focus):not(:active) {
  position: absolute;
  clip-path: inset(50%);
  overflow: hidden;
  block-size: 1px;
  inline-size: 1px;
  white-space: nowrap;
}
补充提示应放在 aria-describedby 中,而不是名称中

一个控件有两种不同的无障碍字符串,混淆二者会导致实际问题:

  • 无障碍名称 — 用户操作所依据的简短主要标签(“在 Facebook 上分享”)。由可见文本、元素内的 visuallyhidden 文本或 aria-label 构成。
  • 无障碍描述 — 在名称之后停顿片刻再读出的次要详情(“在新标签页中打开”)。由 aria-describedby 构成。

将提示合并到名称中会造成三方面的问题:aria-label 会覆盖内部内容(因此,当元素同时有 aria-label 时,visuallyhidden 提示会被静默丢弃);会影响语音控制(用户会说出_名称_来激活控件,而加长后的名称可能不再匹配);还会让每次播报都变得冗长。保持名称简洁,并使用 aria-describedby 附加提示。


<button aria-label="Share on Facebook">
  <svg aria-hidden="true">...</svg>
  <span class="visuallyhidden">opens in a new tab</span>
</button>


<a href="..." target="_blank" rel="noopener" aria-describedby="new-tab-hint"
  >Share on Facebook</a
>


<span id="new-tab-hint" hidden>opens in a new tab</span>

当同一提示适用于多个控件(例如页面上的每个外部链接)时,应在一个全局隐藏的元素中仅渲染一次描述,并通过共享的 id 让各控件指向它,而不是为每个控件重复添加一个 span。这里可以使用 hidden — aria-describedby 仍能解析隐藏的目标。

不要使用 Title 属性

title 属性的支持情况不佳。仅在 <iframe> 上使用。


<button title="Submit form">Submit</button>


<iframe src="..." title="Video player"></iframe>

组件模式

使用已经具备所需行为的原生元素。对于原生 HTML 无法实现的小部件 — 组合框、标签页、开关 — 请参阅 patterns.md,其中包含 APG 参考资料和项目首选方案。

手风琴

使用原生 <details> 和 <summary>。无需 JavaScript。


<div class="accordion">
  <button aria-expanded="false" aria-controls="panel-1">Section</button>
  <div id="panel-1" hidden>Content</div>
</div>


<details>
  <summary>Section</summary>
  <p>Content</p>
</details>
模态对话框

使用原生 <dialog> 和 showModal()。焦点陷阱和 Escape 键处理均已内置。


<div role="dialog" aria-modal="true" aria-labelledby="title">
  <h2 id="title">Title</h2>
  <p>Content</p>
</div>


<dialog id="my-dialog" aria-labelledby="my-dialog-title">
  <h2 id="my-dialog-title">Title</h2>
  <p>Content</p>
  <button type="button" onclick="this.closest('dialog').close()">Close</button>
</dialog>

<button
  type="button"
  onclick="document.getElementById('my-dialog').showModal()"
>
  Open dialog
</button>

showModal() 方法会自动:

  • 将焦点限制在对话框内
  • 按下 Escape 键时关闭
  • 添加 ::backdrop 伪元素
  • 将后方内容标记为 inert

对话框必须具有无障碍名称:为其添加标题,并让 aria-labelledby 指向该标题(或者在没有可见标题时使用 aria-label)。

导航

对于下拉菜单,使用 <nav>、<button> 和 aria-expanded。当页面上有多个 <nav> 时,应根据_其导航目标_为每个导航区命名 — 切勿包含“navigation”一词(参见“不要给地标重复命名”)。

<nav aria-label="Primary">
  <ul>
    <li>
      <button aria-expanded="false" aria-controls="submenu">Products</button>
      <ul id="submenu" hidden>
        <li><a href="/product-1">Product 1</a></li>
      </ul>
    </li>
  </ul>
</nav>

不要将 role="menu"、role="menuitem" 或 aria-haspopup 用于导航。

跳过链接

跳过链接可让键盘和屏幕阅读器用户越过重复区块,直接跳转到主要内容(WCAG 2.2 — 2.4.1 绕过区块)。当网站在多个页面上重复使用复杂导航时,它很有必要 — 例如大型菜单、多级下拉菜单或搜索栏 — 否则这些用户每次加载页面时都必须重新遍历所有内容。如果页面上只有一两个链接作为浏览器界面元素,就没有值得绕过的内容,因此作用不大。

将其设为第一个可聚焦元素,让它指向 <main> 的 id(或主要内容容器的 id),并在获得焦点之前将其隐藏 — 它必须在通过 Tab 键聚焦时变为可见,这样能看见的键盘用户才能看到自己跳转到的位置。

<body>
  <a class="skip-link" href="#main-content">Skip to main content</a>
  <header>...</header>
  <nav aria-label="Primary">...</nav>
  <main id="main-content">...</main>
</body>
.skip-link {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  transform: translateY(-100%);
}

/* Reveal it when a keyboard user tabs to it */
.skip-link:focus-visible {
  transform: translateY(0);
}

不要用 display: none 或 .visuallyhidden 模式隐藏它 — 这些方式在获得焦点后仍会保持隐藏,因此能看见的键盘用户无法看到目标。它需要移到屏幕之外,同时能够通过动画移回视野。

提示

紧急消息使用 role="alert"(强提示);非紧急更新使用 role="status" 或 aria-live="polite"。在注入文本之前,该区域必须已存在于 DOM 中;不要在 role="alert" 上额外手动添加 aria-live/aria-atomic — 这些属性已隐含其中(WCAG 2.2 — 4.1.3 状态消息)。标记、完整的礼貌级别说明以及触发相关注意事项:请参阅 patterns.md。

表单错误

不要用 aria-live 宣布验证错误,而应将用户_带到_错误所在位置。使用 aria-invalid="true" + aria-describedby 将行内消息与对应字段关联起来;提交失败时,将焦点移至表单顶部的错误摘要(tabindex="-1")。标记和原因:patterns.md。

轮播 / 可滚动内容

使用原生 HTML 构建轮播,不要使用轮播库,也不要手动编写轮播 ARIA。优先使用带有 overflow-x: auto + CSS 滚动捕捉的 <ul>,让浏览器负责键盘和触控滚动;对于已无法继续前进的箭头,使用 aria-disabled(而非原生 disabled)。本项目默认采用原生列表 / 滚动语义,而非 role="region" + aria-roledescription。滚动捕捉轨道和单张幻灯片切换的标记和原因:patterns.md。


CSS 指南

这些 CSS 选择会直接影响无障碍性。以下每条规则的完整代码示例和原因见 css.md。

使用相对长度单位,而非绝对 px

固定像素尺寸会忽略用户的字号偏好,并在 200% 缩放和重新排版时造成问题(WCAG 2.2 — 1.4.4 调整文本大小、1.4.10 重新排版)。默认使用 rem 设置字体、间距和布局;使用 em 设置随元素自身缩放的值;使用 %/ch/视口单位实现流式布局;仅将 px 用于极细边框和光学微调。

使用逻辑属性

优先使用逻辑属性(inline-size、margin-inline、inset-inline-start),而非物理属性(width、margin-left、top),这样布局无需重写即可适应从右到左和竖排书写模式。

使用 ARIA 属性作为样式钩子

直接为 ARIA 状态设置样式([aria-expanded="true"]、[aria-current="page"]、[aria-disabled="true"]),而不是添加平行的修饰类,这样只有一个事实来源,不会与无障碍树中的状态不一致。

让光标传达交互性

为可交互控件设置 cursor: pointer,为非活动控件设置 cursor: not-allowed。由于按钮使用 aria-disabled 而非原生 disabled 表示“关闭”(见“绝不禁用按钮”),应为该状态明确设置样式。光标只是仅面向指针用户的辅助提示,应与实际语义并存,绝不能取代语义。

不要在 HTML 中使用全大写

使用常规大小写,并通过 CSS 将文字转为大写(text-transform: uppercase),这样屏幕阅读器就不会逐个读出字母。

满足颜色对比度要求

文本必须满足 WCAG 2.2 AA 最低对比度要求,这是最常未通过的检查之一。使用 WebAIM 对比度检查器验证颜色搭配。最低要求:

  • 普通文本(小于 18pt / 粗体小于 14pt):4.5:1
  • 大号文本(18pt 及以上 / 粗体 14pt 及以上):3:1
  • UI 组件和焦点指示器:与相邻颜色的对比度为 3:1

css.md 提供了安全的默认调色板,无需计算即可满足这些要求。

创建一致的焦点轮廓

通过 :focus-visible 为每个可交互元素提供清晰、高对比度的焦点指示器(例如 outline: 2px solid; outline-offset: 2px),让键盘用户能看到焦点环,同时避免鼠标点击时闪现。

尊重减少动态效果和减少透明度偏好

将动画限制在 @media (prefers-reduced-motion: no-preference) 内,将玻璃质感 / 半透明效果限制在 @media (prefers-reduced-transparency: no-preference) 内,这样选择关闭这些效果的用户就不会看到它们。

安全地淡入内容

不要仅使用 opacity: 0 隐藏淡入前的内容——屏幕阅读器会忽略透明度,因此视力正常的用户尚未看到元素时,屏幕阅读器就已将其读出。将隐藏状态限制在 JS 就绪类后,并使用 IntersectionObserver 触发动画,这样当屏幕阅读器的虚拟光标滚动到元素可见位置时,动画就会启动。

本地参考规范(references/)

所需的引用内容都在 references/ 中以本地离线权威资料的形式提供。wcag.json 是完整的 WCAG 2.2 规范结构化数据——按准则 ID 查询,切勿通读整个文件。 aria.md 是大型的规范原文转储——切勿从头读到尾;请搜索所需术语。 standards.md 索引了规范的在线来源,patterns.md 则收录了原生 HTML 无法涵盖的组件模式。

各技术栈文件(react.md、vue.md、svelte.md、angular.md、web-components.md)是上文“适配项目技术栈”部分的另一半——读取与检测到的框架相匹配的文件。每个文件都很短,只介绍相对于 HTML 基准的差异(属性转换、ID 生成、路由焦点、模板 XSS 逃逸机制、组件库注意事项),不会重复通用规则。

在 wcag.json 中查找 WCAG 准则

wcag.json 是一个包含四项原则的 JSON 数组;每项原则下嵌套 guidelines,每项指南下嵌套 success_criteria。每个节点都包含 ref_id("1"、"1.4"、"1.4.3")、title、description 和 url;成功准则还包含 level(A/AA/AAA)、special_cases、notes 和 references。请按 ref_id 而不是行号查找准则,并使用 jq 提取完整对象:

jq '.. | objects | select(.ref_id? == "1.4.3")' references/wcag.json

如果只知道名称,请搜索标题('Contrast (Minimum)'、'Bypass Blocks')。其中包含截至 AAA 级的所有成功准则——包括 2.5 Input Modalities、2.4.2 Page Titled 和 2.4.6 Headings and Labels——因此,如果某个 ref_id 不在其中,该准则就不存在于 WCAG 2.2 中。

在 aria.md 中搜索 ARIA 规范

aria.md 是规范的 Markdown 原文转储——请按内容关键词搜索,而不是猜标题。搜索角色或属性本身(aria-expanded、Name, Role, Value)——这样总能找到匹配项——然后阅读匹配项附近的内容,或查看下表中已知范围的内容。

文件规范(大小)适用场景关键章节(行范围)
standards.md外部链接索引(约 1 KB)需要引用权威在线来源时——WCAG 理解文档、快速参考、WAI-ARIA 规范、APG、WebAIM、W3C 验证器通读全文;内容很短
react.md · vue.md · svelte.md · angular.md · web-components.md各技术栈的无障碍差异(每份约 2–3 KB)项目使用了相应框架时——将 HTML 示例转换为该框架惯用的写法,并避免该框架特有的陷阱(路由焦点、模板 XSS、id/阴影边界规则)阅读与技术栈匹配的文件全文;每份都很短
patterns.md组件模式(约 5 KB)查看正文概述的组件的完整标记和原理——轮播、实时区域和警报、表单错误——以及组合框、标签页、开关和 APG 链接通读全文;内容很短
css.mdCSS 无障碍模式(约 6 KB)查看正文概述的任何 CSS 规则的完整代码和原理——相对单位、逻辑属性、ARIA 样式钩子、光标、对比度调色板、焦点轮廓、减少动态效果/透明度、安全淡入通读全文;内容很短
wcag.jsonWCAG 2.2,结构化 JSON——AA 是项目基线检查成功标准:对比度、焦点顺序、标签、错误处理、状态消息原则数组 1–4 → guidelines → success_criteria,每个节点都以 ref_id 为键。使用 jq/grep 按 ref_id 或 title 查询——不要通读全文。包含直至 AAA 级别的所有标准。
aria.mdWAI-ARIA 1.2(515 KB)仅在必须手动编写 ARIA 时(很少见——原生元素自带角色和状态)使用。添加属性前,确认相应角色支持该属性4. Using WAI-ARIA 464–568 · 5. The Roles Model 569–2804(角色定义从 800 行开始)· 6. Supported States & Properties 2805–4344(aria-* 定义从 2916 行开始)· 7. Accessibility Tree 4345–4373 · 8. Implementation in Host Languages 4374–4472
对于教程式指南和键盘操作模式,在线版 WCAG 2.2 Understanding 和 ARIA APG 比原始规范更易读。使用 WebAIM Contrast Checker 验证颜色搭配。

评论

支持 Markdown