
在实际前端工程里真正难的不是学会某个无障碍 API而是让“无障碍要求”在跨角色、跨版本、跨模块的开发过程中始终不丢失。A11y.md 解决的就是这个问题它把项目要遵守的无障碍规则、组件使用约定、验收标准和测试方式沉淀为仓库内一组可阅读、可审查、可被 CI 调用的上下文文件。围绕这套上下文系统团队不再需要靠某位精通无障碍的同事口头提醒也不需要翻聊天记录找设计决策。下面从问题出发看这套系统应该怎么设计、怎么落地、怎么验证以及生产环境里最容易踩哪些坑。无障碍能力在大多数项目里并不是一开始就没有而是随着迭代被慢慢磨掉的。设计稿换了颜色、组件库升级了 API、新同学不理解某个旧组件的键盘交互这些细小的变化累积到一定阶段产品就会从“基本可用”退化成“鼠标用户能点、键盘用户和读屏用户被卡住”的状态。A11y.md 的意义就是把这些信息从人的记忆和评审会里挪出来放进代码仓库成为代码变更流程的一部分。1. 先理解为什么无障碍能力需要一个上下文系统1.1 无障碍上下文是什么“上下文”这个说法可能有点抽象。放到具体场景里它指的是一个按钮为什么需要 focus 样式、一个弹窗为什么要在打开时把焦点移进去、一个表格为什么不能只靠颜色区分状态这些东西单独看是零散的知识点但放在一起就构成了某个页面的无障碍语境。A11y.md 把这种语境文件化。它可以是一个仓库根目录下的a11y.md也可以是一组docs/a11y/下的分模块文档。每个文件描述一个组件或一个功能的无障碍要求包括这个组件服务哪些用户场景。需要满足哪些 WCAG 成功标准。键盘操作矩阵是什么样的。读屏软件下的预期播报是什么。自动化检查和人工验收分别怎么做。它不是《无障碍入门手册》而是“当前项目当前模块的具体要求”。手册讲通用原则上下文文件讲本项目落地。1.2 为什么只用规范文档不够很多人会反驳直接让团队读 WCAG 不就行了为什么还要多做一套文件这里的问题是 WCAG 是标准不是执行方案。标准不会告诉你“橙色背景上的白色文字在产品里到底可用不可用”也不会告诉你“这个自定义下拉框要用aria-activedescendant还是直接把真实焦点移给选项”。项目需要的是经过取舍后的实现约定。另一个问题是信息分散。无障碍要求可能散落在设计稿的批注里。交互文档的脚注里。某个 Pull Request 的评审意见里。某次事故后的修复代码里。这些地方没有统一的查询入口。等三个月后另一个同学重做同一个组件他大概率看不到这些历史决策于是再次引入同样的缺陷。上下文系统恰好提供了唯一入口和默认约定。1.3 学习环境与生产环境的区别在个人练习项目里无障碍知识可以用零散笔记记录在生产项目里必须有明确的责任人和生命周期。学习场景下一个a11y.md更多是给自己看的备忘生产场景下它必须承担三个额外职责作为组件开发、修改、重构时的必读材料。作为代码审查时的对照清单。作为自动化测试和人工验收的目标来源。这决定了生产环境的 A11y.md 不能是自由文本应该有结构、有字段、有检查脚本。没有结构就无法探测缺失没有检查脚本就无法进入流程。2. 设计 A11y.md 上下文系统的整体结构2.1 先从目录结构开始上下文系统的落地形态可以很轻。常见做法是在仓库根目录放一个总入口a11y.md再在docs/a11y/下按组件或功能域拆分子文件。以下是一个可以直接参考的结构. ├── a11y.md ├── docs │ └── a11y │ ├── _template.md │ ├── button-a11y.md │ ├── form-input-a11y.md │ ├── dialog-a11y.md │ ├── navigation-a11y.md │ └── adr-0001-keyboard-navigation.md ├── scripts │ └── check-a11y-context.sh └── package.json根目录的a11y.md承担两个作用第一用不超过二十行的篇幅说明项目对无障碍的基本态度和免责边界第二引导读者去查看更具体的分模块文件。_template.md是新建上下文文件时的模板避免每个组件写得格式不同。scripts/check-a11y-context.sh是校验脚本供本地和 CI 调用。这种结构避免了“一个大文档几百行改起来不敢动”的问题。组件级上下文按文件拆分修改时只影响相关文件评审也更容易聚焦。2.2 文件中应该承载哪些职责不是所有无障碍信息都应该放进 A11y.md。设计原则是可执行的约定放进来一次性决策只记结论通用知识放外链。文件或目录职责指向什么根目录a11y.md项目级无障碍原则、术语、入口docs/a11y 细分文件、外部标准docs/a11y/_template.md新建上下文文件的统一模板原子文档docs/a11y/*-a11y.md单个组件或功能域的具体要求组件源码、测试用例、设计稿docs/a11y/adr-*.md记录无障碍方案的选择与取舍当时的评审、遗留约束scripts/check-a11y-context.sh检查字段、状态和关联文件是否完整CI 流水线这里要特别注意“ADR”文件。A11y.md 记录当前要求ADR 记录这个要求是怎么来的。例如为什么不用原生select而用自定义下拉框为什么某些场景下允许把颜色作为唯一信息。这些决策如果不写下以后一定会有人问。2.3 粒度怎么选上下文文件拆多细没有绝对标准。建议按“一个文件对应一个可独立测试的组件或页面区域”来切。按钮、输入框、弹窗、侧边导航这些是组件级注册流程、搜索结果页、支付页这些是页面级。初期不要试图覆盖所有组件。先把“被修改频率高、无障碍风险大、已经有历史缺陷”的组件纳入。比如表单控件、弹窗、导航、图表这四类优先级明显高于装饰性组件。上下文文件不是越多越好而是越准越好。一个无人维护的上下文文件比没有文件更糟糕因为它会给团队虚假的安全感。3. 从零落地一套最小可运行的 A11y.md 上下文系统3.1 在仓库根目录创建总入口先创建一个最小但完整的a11y.md。文件不需要一上来就覆盖全项目但要交代清楚适用范围和查询方式。--- title: 项目无障碍上下文入口 status: ready owner: frontend-core wcag: [2.1, 2.4, 4.1] review_cycle_days: 90 --- # 项目无障碍上下文入口 本项目将无障碍作为基础质量属性。任何修改组件交互、视觉表现、 表单行为、焦点管理的变更都应先阅读对应的上下文文件。 ## 如何使用本文件 - 开发组件前阅读 docs/a11y/ 下对应组件条目。 - 提交变更前确认相关上下文的 status 和 last_reviewed 是否需要更新。 - 测试用例在自动化测试中引用上下文内的关键成功标准。 ## 当前覆盖范围 | 组件 | 文件 | 状态 | | --- | --- | --- | | 按钮 | docs/a11y/button-a11y.md | ready | | 表单输入 | docs/a11y/form-input-a11y.md | ready | | 弹窗 | docs/a11y/dialog-a11y.md | in_review | | 导航 | docs/a11y/navigation-a11y.md | draft |这里的 YAML frontmatter 是为了让脚本可以自动检查。不要小看status字段它直接决定了这份文档是“只给人类看的参考”还是“可以进入自动化流程的事实依据”。3.2 创建第一个组件上下文文件下面以按钮组件为例写一个最小可用的上下文文件。实际项目里按钮看起来简单但焦点样式、对比度、状态提示、图标按钮的无障碍名称都是容易出问题的点。--- id: button-a11y title: 按钮组件无障碍上下文 status: ready owner: design-system-team impact: - Button - IconButton - SplitButton wcag: - 1.4.3 - 1.4.11 - 2.1.1 - 2.4.7 - 4.1.2 last_reviewed: 2025-01-10 --- ## 使用场景 按钮组件服务于所有需要触发操作命令的界面。图标按钮必须提供 可访问名称不能只依赖视觉形状。 ## 键盘操作矩阵 | 按键 | 行为 | 状态 | | --- | --- | --- | | Tab | 焦点进入按钮显示可见焦点 | 必须 | | Enter | 触发按钮主操作 | 必须 | | Space | 触发按钮主操作不能触发页面滚动 | 必须 | ## 读屏要求 - 按钮的可访问名称应等于可见文本没有可见文本时使用 aria-label 或 aria-labelledby 补充。 - 禁止使用 title 作为唯一的名称来源。 ## 自动化检查命令 bash npx axe --exit --rules color-contrast人工验收清单键盘按 Tab 可以到达焦点环清晰。使用读屏软件朗读时能听到明确按钮名称。禁用态按钮不会被读成普通按钮且不会接收焦点。这段内容的价值在于把抽象标准转化为具体断言。“2.1.1 键盘可达”是一个抽象标准具体到这个项目就是 Tab 能到达、Enter 和 Space 能触发、Space 不能滚页面。评审时直接对照表格打勾即可。 ### 3.3 写一个轻量校验脚本 如果上下文文件只有 Markdown很容易在三个月后变得没人更新。所以需要一个脚本在本地开发、提交前和 CI 三个阶段检查文件是否满足最低要求。下面是一个简化示例用 Bash 实现适合放到任意仓库里。 bash #!/usr/bin/env bash set -euo pipefail ROOT_DIR$(cd $(dirname $0)/.. pwd) CONTEXT_DIR${ROOT_DIR}/docs/a11y ENTRY_FILE${ROOT_DIR}/a11y.md FAILED0 echo 检查根入口 a11y.md if [ ! -f $ENTRY_FILE ]; then echo 缺少 a11y.md FAILED1 fi echo 检查组件上下文关键字段 for file in $CONTEXT_DIR/*-a11y.md; do echo 检查 $file grep -q ^id: $file || { echo 缺少 id; FAILED1; } grep -q ^status: $file || { echo 缺少 status; FAILED1; } grep -q ^wcag: $file || { echo 缺少 wcag; FAILED1; } grep -q ^owner: $file || { echo 缺少 owner; FAILED1; } grep -q ^last_reviewed: $file || { echo 缺少 last_reviewed; FAILED1; } done echo 检查根入口中列出的组件文件是否存在 while read -r path; do if [ ! -f $CONTEXT_DIR/$path ]; then echo 根入口中引用了不存在的文件$path FAILED1 fi done (grep -oE docs/a11y/[a-z-]-a11y\.md $ENTRY_FILE | sed s#docs/a11y/## | sort -u) if [ $FAILED -ne 0 ]; then echo A11y 上下文检查失败 exit 1 fi echo A11y 上下文检查通过这个脚本只做了三件事检查总入口是否存在、检查每个组件上下文是否含有关键字段、检查总入口里的文件引用是否真实存在。它不判断内容对错只保证“该有的形状还在”。注意脚本的字段检查要保持在“过少会失效、过多会无人维护”之间的平衡。设置为固定字段加上少量权重字段是最稳妥的起步方案。3.4 为什么这样设计最小闭环很多人容易把上下文系统做得太重一上来就写规范、建平台、做可视化页面。最小闭环的原则是先让一个组件的上下文文件进入仓库然后再让一个脚本能检查这个文件最后让这个脚本进入 CI。这样整个系统即使只有十个文件也能运转后续扩展只是增加文件和规则而不是推翻架构。学习环境里可以只保留a11y.md和docs/a11y/template.md两个文件用来验证流程生产环境里再按组件逐个补全避免一开始就陷入写文档的体力劳动。4. 把上下文接入开发工作流4.1 用任务模板和 PR 模板强制关联上下文系统最大的敌人是“与我无关”。要解决这个问题第一步不是写更多文档而是把上下文引用变成任务和代码评审的必填项。在 Issue 模板中增加一段### 无障碍影响评估 - [ ] 该变更会修改组件交互或视觉表现 - [ ] 已查看 docs/a11y 下对应组件上下文文件 - [ ] 若组件上下文状态需要更新已在本次变更中更新在 Pull Request 模板中增加### A11y 上下文核对 | 检查项 | 说明 | | --- | --- | | 影响的组件 | Button、Dialog | | 上下文文件 | docs/a11y/button-a11y.md、docs/a11y/dialog-a11y.md | | 上下文状态是否更新 | 是 / 否 | | 是否补充或调整了自动化测试 | 是 / 否 |这段不是走形式。它的作用是迫使开发者去查看对应文件哪怕最后结论是“不需要更新”查看动作本身也会降低遗漏概率。4.2 在 CI 中执行上下文检查在 GitHub Actions 或类似流水线中上下文检查可以和代码检查并行执行。下面的 YAML 片段演示了如何配置一个独立 job。name: a11y-context-check on: pull_request: paths: - ** push: branches: - main jobs: check-a11y-context: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Run a11y context validator run: bash scripts/check-a11y-context.sh - name: Lint markdown run: npx markdownlint-cli2 *.md docs/a11y/**/*.md这里的关键点是paths: [**]。不要只监听docs/a11y/**因为组件代码变更时也需要知道上下文是否过期。哪怕这次只是修改一个颜色变量CI 也应该提示开发者确认对比度上下文是否仍然有效。4.3 把上下文引用进自动化测试上下文文件不能只停留在文档层。更高级的用法是让测试用例引用上下文里的断言目标。比如在 Cypress 或 Testing Library 测试里可以直接把 WCAG 成功标准作为测试描述的一部分。describe(按钮组件无障碍要求, () { it(应满足按钮上下文文件中的键盘操作矩阵Tab 可到达, () { cy.visit(/components/button); cy.get(button.primary).focus(); cy.focused().should(have.class, primary); }); it(应满足 1.4.3 对比度要求, () { cy.visit(/components/button); cy.get(button.primary).then(($btn) { const bg $btn.css(background-color); const color $btn.css(color); cy.task(checkColorContrast, { bg, color }).then((score) { expect(score).to.be.greaterThan(4.5); }); }); }); });测试负责验证可执行的部分但测试无法验证所有事情。焦点顺序是否合理、读屏播报是否清晰、禁用态的处理是否符合场景这些仍然是人工验收的范畴。因此 A11y.md 要同时包含“可自动化断言”和“需人工验收”两个区域避免团队误以为测试通过就等于无障碍达标。4.4 常见的切入点位接入工作流并不是一步到位可以从几个容易出成果的切入点开始新组件开发时必须创建对应上下文文件。组件库发版前检查上下文状态是否为ready否则阻止发布。修改了样式 token 时CI 自动触发对比度检查。每个迭代末人工 review 一次status: draft的上下文文件。这些切入点都遵循同一个原则让无障碍要求尽快变成“流程中的红线”而不是“文档里的建议”。5. 上下文文件的内容模型与字段设计5.1 前端字段的含义上下文文件需要一组结构化字段支持自动检查。我建议保留下面这些字段它们覆盖了大多数组件场景字段必填含义示例值id是文件唯一标识用于交叉引用button-a11ytitle是人类可读的标题按钮组件无障碍上下文status是文件生命周期状态ready、in_review、draft、deprecatedowner是责任人团队名或 GitHub 用户名frontend-coreimpact是受影响组件或页面列表Button、IconButtonwcag是对应的 WCAG 成功标准列表1.4.3、2.4.7last_reviewed是最近一次人工审查日期2025-01-10related_files否关联源码、测试、设计稿路径src/components/Buttonreview_cycle_days否定期复审周期90owner很重要。没有 owner 的文档在出问题时会进入“谁都觉得是别人的事”的状态最终无人更新。last_reviewed保证文档有时间感配合 CI 可以提示过期文件。5.2 状态机如何流转上下文的status不应该随意写。建议采用一个简单的状态机状态含义进入条件可执行动作draft草稿信息不完整新建文件仅编辑in_review内容已成型正在评审指定 owner 发起评审可评审不可作为发布依据ready已验证可用于开发验收评审通过测试补充完成可作为发布和验收依据deprecated已被替代仅保留历史决策组件下线或策略变更不再引用保留供审计状态机要配合 CI比如ready状态的文件缺失last_reviewed时应该直接失败。反过来draft状态允许字段不完整避免团队因为流程太重而不愿意新建文件。5.3 参数设计常见错误错误做法问题推荐做法impact直接抄组件库所有组件字段失去指向性变更同步时无法定位只列实际受影响的组件wcag写一堆用不到的条目文档变成百科全书没人对照只写当前实现真正验证的条目owner写整个团队出问题找不到具体负责人写到一个明确个人或小组没有last_reviewed文档过期无法被识别必须要求日期配合周期性检查这些细节决定上下文系统是否能长期运转。结构化不是目的让自动化检查可判定才是目的。6. 运行验证与效果衡量6.1 本地验证方式在本地开发阶段可以通过一条命令完成上下文系统的自检bash scripts/check-a11y-context.sh预期输出是 检查根入口 a11y.md 检查组件上下文关键字段 检查 docs/a11y/button-a11y.md 检查 docs/a11y/form-input-a11y.md 检查根入口中列出的组件文件是否存在 A11y 上下文检查通过如果某个文件缺少字段脚本会中断并给出明确文件名。这个阶段的价值在于把“文件是否格式规范”的判断交给脚本而不是交给人的记忆。6.2 结合组件测试的运行验证当自动化测试也接入了上下文后验证命令会变成npm run test:unit npm run test:a11y bash scripts/check-a11y-context.sh这里不要只看测试失败数量。更重要的是观察失败信息是否指向具体上下文。比如“按钮组件未满足 1.4.3 对比度”比“页面存在一个对比度错误”更有指导性。测试用例的标题如果能带出 WCAG 标准号失败后的排查效率会明显提升。6.3 文档维护清单建议每个迭代或每次发布前按下面的清单做一次轻量检查根目录a11y.md中的覆盖范围表格是否与docs/a11y目录实际文件一致。是否存在status: ready但last_reviewed超过review_cycle_days的文件。是否存在已合并 PR 中修改了交互行为但对应上下文文件没有更新记录。是否存在组件已删除但上下文文件仍然存在且状态不是deprecated。新增组件是否在根目录表格中登记并为新组件创建了上下文文件。这份清单可以做成脚本定期提醒也可以放进每月技术评审会议里。关键不是“每次全查”而是“每次至少确认一项”让文档持续与代码保持同步。注意上下文系统的效果不以“创建了多少个文件”衡量而以“组件变更时是否有对应记录可查”衡量。后者才是真正降低返工成本的信号。7. 常见问题与排查路径7.1 上下文文件写好了但团队从来不更新现象docs/a11y/下有几份文件内容也很完整但三个月内没有一次提交修改这些文件。排查顺序检查任务模板和 PR 模板里是否真的加入了“查看并更新上下文”的勾选项。很多团队的模板只是挂在那里并没有执行 enforce。检查 CI 是否真的会在非docs/**路径变更时运行上下文检查。检查团队是否知道文件位置。如果搜索a11y.md只能从根目录导航发现说明入口不够显眼。检查上下文文件本身是否过长、过抽象导致没人愿意打开阅读。解决方案把上下文引用绑定到实际代码路径。比如在Button组件源码第一行加注释// 修改此组件前阅读 docs/a11y/button-a11y.md这样修改代码的人不可能错过。同时把 CI 检查从“只检查文件存在”升级为“组件文件变更时对应上下文必须变更”。7.2 文档和代码不一致上下文成了历史资料现象代码已经采用新的实现方案但上下文里的键盘矩阵和读屏描述还是旧的。原因上下文文件的更新没有被纳入变更流程开发者修改代码时认为这是“另一个领域的文件”。解决方案分两层。第一层是流程让 CI 对“上下文状态”变化敏感代码 PR 中如果有组件交互相关文件变更自动贴上a11y-context标签提醒作者更新对应文档。第二层是技术把上下文文件迁移到组件目录内和组件源码放在一起。比如src/components/Button/ ├── Button.tsx ├── Button.test.tsx └── a11y.md这样修改组件时同一目录下就能看到上下文文件避免“docs 目录与我无关”的错觉。但这种方法会让 CI 扫描范围变多适合组件库项目普通业务项目可以权衡。7.3 CI 跑过了结果还是错的现象上下文文件字段齐全测试也通过但上线后仍然收到无障碍缺陷反馈。排查路径检查自动化测试是否只覆盖了简单的颜色对比和标签存在没有覆盖焦点顺序、读屏播报、交互逻辑。检查测试运行环境是否使用了真实浏览器。jsdom 里很多无障碍行为无法模拟。检查人工验收流程是否真的执行。上下文里写了“人工验收清单”但没有安排对应角色去验证。检查读屏测试的辅助技术版本。不同读屏软件对 ARIA 的解析有差异上下文应标注“以哪套辅助技术版本为准”。解决方案把上下文文件中的断言分成两层automated层和manual层。自动化层由 CI 强制手册层由迭代计划强制。发布条件不应该是“CI 通过”而应该是“automated 断言通过 manual 清单在最近一次迭代里完成”。7.4status: ready被人为刷上去现象团队为了满足 CI 门槛把文档状态从draft直接改成ready内容并不充分。原因状态变成了流程关卡而不是质量标签。开发者只想让构建变绿。解决方案给ready状态设置额外的硬条件。比如 CI 不但检查status字段还检查文件中是否包含“键盘操作矩阵”和“人工验收清单”两个小节不出现就失败。这样可以减少为通过而修改状态的动机。grep -q ## 键盘操作矩阵 $file || { echo 机器提示ready 状态需要键盘矩阵; FAILED1; } grep -q ## 人工验收清单 $file || { echo 机器提示ready 状态需要人工验收清单; FAILED1; }这一条不一定完全杜绝问题但能把“随便写两句就 ready”的成本抬高。8. 最佳实践与扩展方向8.1 从最小范围开始滚动维护第一次搭建 A11y.md 上下文系统时不要追求覆盖所有页面。先把最高频组件纳入运行一个迭代验证流程再逐步推广。很多团队失败是因为一次性写了二十几份文档却没有一个组件真正按文档改过代码最后所有文档都成了摆设。推荐的做法是第 1 个迭代创建根入口和 1 个组件的上下文文件。第 2 个迭代接入 CI 脚本和 PR 模板。第 3 个迭代把测试用例与上下文对应起来。后续每迭代新增 2 到 3 个组件同时清理过期文件。8.2 与设计系统、ADR 相结合上下文系统在组件库项目中的价值最大。组件库是所有业务页面的公共底座一个按钮组件的焦点样式改坏了影响的是几十个页面。A11y.md 可以作为组件库发布门禁的一部分组件文档不满足上下文要求就不允许进入发布队列。同时建议把无障碍决策作为 ADR 沉淀。比如“为什么这里使用aria-livepolite而不是assertive”“为什么禁用态按钮不保留 focus”这些决策如果只写在上下文里可能太细拆成独立 ADR 后未来任何方向性讨论都能引用。8.3 扩展成可观测的质量门禁进阶方向是让上下文系统参与质量度量。可以建立一个简单的统计任务每天扫描一次仓库记录以下内容ready状态组件数量。draft状态组件数量。过期未复审的上下文数量。上下文文件与组件源码路径的匹配率。不用把这些数据做成大屏只需要在每周日报里出现一个数字。只要“过期上下文数量”持续不归零就说明团队还有技术债需要处理。8.4 最后一点实践建议A11y.md 的本质不是文档而是通信机制。它把无障碍要求从“某个人懂”变成“整个流程都参考它”。落地时最有价值的动作不是写一篇漂亮的总纲而是让修改代码的人在一个看得见的位置看到这句提示这个组件有无障碍上下文要求改动前先读它。只要这个提示真正出现在代码评审、任务模板和测试用例里上下文系统就已经开始发挥作用了。下一步可以做的扩展包括在 Storybook 里增加 A11y 文档页、在组件示例里嵌入 axe-core 扫描结果、把a11y.md作为组件 API 契约的一部分纳入类型检查工具。实际操作时仍然建议先保持文件、脚本、CI 三件套足够轻再根据团队节奏逐步加重。