27e - 响应式布局与无障碍
本文是《AI Agent 实战手册》第 27 章第 5 节。 上一节:27d-设计系统与组件库维护 | 下一节:27f-前端Steering规则与反模式
概述
响应式布局和无障碍(Accessibility, a11y)是前端开发中最容易被”先跳过、后补救”的两个领域——而补救成本往往是初始实现的 5-10 倍。2025-2026 年,随着欧洲无障碍法案(EAA)正式生效、美国 ADA Title II 截止日期临近、WCAG 2.2 成为法律标准,无障碍已从”加分项”变为”法律义务”。与此同时,CSS Container Queries、View Transitions API、@starting-style 等现代 CSS 特性的全面落地,加上 AI 编码助手对布局和动画代码的生成能力大幅提升,使得”AI 辅助响应式 + 无障碍”成为前端 Vibe Coding 的核心工作流。本节系统覆盖 AI 辅助的响应式布局生成、CSS 动画与过渡效果、无障碍实现与审查,以及 WCAG 合规检查的完整工具链和 prompt 模板。
1. AI 辅助响应式布局生成
1.1 现代响应式布局技术栈
2025-2026 年的响应式布局已经从”媒体查询为主”演进到”容器查询 + 媒体查询 + 内在尺寸”的三层体系:
| 技术 | 适用场景 | 浏览器支持 | AI 生成难度 |
|---|---|---|---|
| Media Queries | 全局布局断点(移动/平板/桌面) | 全部 | ⭐ 低 |
Container Queries (@container) | 组件级响应式(侧边栏/卡片/网格项) | Chrome 105+, Safari 16+, Firefox 110+ | ⭐⭐ 中 |
CSS Grid + auto-fit/auto-fill | 自适应网格布局 | 全部 | ⭐ 低 |
Flexbox + flex-wrap | 流式排列 | 全部 | ⭐ 低 |
clamp() / min() / max() | 流体排版和间距 | 全部 | ⭐⭐ 中 |
| View Transitions API | 页面/状态切换动画 | Chrome 111+, Safari 18+ | ⭐⭐⭐ 高 |
@starting-style | 元素进入动画 | Chrome 117+, Safari 17.5+ | ⭐⭐ 中 |
1.2 工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| v0.dev | AI 生成响应式 UI 组件 | 免费(有限额)/ Pro $20/月 | 快速原型、Tailwind 组件 |
| Cursor | AI 编码助手,支持响应式代码生成 | 免费 / Pro $20/月 | 日常开发、代码补全 |
| Claude Code | Agentic 编码,理解项目上下文 | 按 token 计费 | 复杂布局重构、全项目响应式改造 |
| Kiro | Spec-Driven 开发,Steering 规则 | 免费(预览期) | 规范化响应式开发流程 |
| Windframe | Tailwind CSS 可视化构建器 | 免费 / Pro $12/月 | 可视化拖拽响应式布局 |
| Workik | AI CSS/Tailwind 代码生成器 | 免费 | 快速生成 Grid/Flexbox/媒体查询 |
| Responsively App | 多设备同步预览 | 免费(开源) | 响应式调试和测试 |
| Polypane | 多视口浏览器 | $14/月起 | 专业响应式开发和无障碍测试 |
1.3 操作步骤:AI 辅助响应式布局工作流
步骤 1:定义断点策略
在项目启动时,先用 AI 生成统一的断点系统。这是响应式布局的基础。
提示词模板:
你是一位前端架构师。请为我的 [React/Vue/Svelte] 项目设计一套响应式断点系统。
项目信息:
- CSS 框架:[Tailwind CSS v4 / 原生 CSS / CSS Modules]
- 目标设备:[手机、平板、桌面、大屏]
- 设计稿宽度:[375px (移动) / 768px (平板) / 1440px (桌面)]
请提供:
1. 断点定义(使用 CSS 自定义属性或 Tailwind 配置)
2. 每个断点的典型布局模式(单列/双列/三列等)
3. 流体排版方案(使用 clamp() 函数)
4. Container Query 的使用建议(哪些组件适合用容器查询)
5. 一个 _breakpoints.css 或 tailwind.config.ts 的完整配置文件步骤 2:AI 生成响应式组件
对于具体组件,使用分层 prompt 策略——先描述布局意图,再指定响应式行为:
提示词模板:
请为以下组件生成响应式布局代码:
组件:[产品卡片网格 / 导航栏 / Hero 区域 / 仪表板布局]
框架:[React + Tailwind CSS v4]
响应式需求:
- 移动端 (<640px):[单列堆叠,图片在上文字在下]
- 平板 (640px-1024px):[两列网格,间距 16px]
- 桌面 (>1024px):[三列网格,间距 24px,最大宽度 1280px]
技术要求:
- 使用 CSS Grid + auto-fit 实现自适应列数
- 图片使用 aspect-ratio 保持比例
- 文字使用 clamp() 实现流体排版
- 为卡片组件添加 Container Query 支持(当容器宽度 < 300px 时切换为紧凑模式)
- 确保所有交互元素的触摸目标 ≥ 44x44px(WCAG 2.2 要求)步骤 3:Container Query 组件化
Container Queries 是 2025-2026 年响应式布局的核心进化——让组件根据自身容器大小而非视口大小来调整布局。这对组件库和设计系统尤为重要。
提示词模板:
请将以下组件重构为使用 CSS Container Queries 的自适应组件:
当前组件:[粘贴现有组件代码]
要求:
1. 将父容器标记为 container(container-type: inline-size)
2. 定义至少 3 个容器断点:
- 紧凑模式 (< 300px):[描述布局]
- 标准模式 (300px - 500px):[描述布局]
- 宽屏模式 (> 500px):[描述布局]
3. 使用 @container 查询替代 @media 查询
4. 确保组件在任何容器宽度下都可用
5. 添加 Tailwind CSS v4 的容器查询语法(@container 变体)步骤 4:响应式布局审查
生成代码后,使用 AI 进行响应式布局审查:
提示词模板:
请审查以下响应式布局代码,检查常见问题:
[粘贴组件代码]
审查清单:
1. 是否有硬编码的像素宽度导致溢出?
2. 媒体查询断点是否一致且无间隙?
3. 图片/视频是否有 max-width: 100% 和适当的 aspect-ratio?
4. 触摸目标是否满足 44x44px 最小尺寸?
5. 文字是否使用相对单位(rem/em/clamp)而非固定 px?
6. Flexbox/Grid 是否正确处理了内容溢出?
7. 是否考虑了横屏模式?
8. Container Query 的 container-type 是否正确设置?
9. 是否有不必要的 overflow: hidden 截断内容?
10. 间距系统是否使用了设计 Token 而非魔法数字?1.4 响应式布局代码示例
以下是一个 AI 生成的现代响应式卡片网格示例,综合使用了 Container Queries、CSS Grid 和流体排版:
/* 断点系统 */
:root {
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
/* 流体排版 */
--font-size-body: clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
--font-size-heading: clamp(1.25rem, 1rem + 0.75vw, 1.75rem);
/* 流体间距 */
--space-md: clamp(0.75rem, 0.5rem + 0.5vw, 1.5rem);
--space-lg: clamp(1rem, 0.75rem + 1vw, 2rem);
}
/* 卡片网格容器 */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: var(--space-md);
padding: var(--space-lg);
}
/* 卡片组件 - 使用 Container Query */
.card {
container-type: inline-size;
container-name: card;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
}
.card__inner {
display: flex;
flex-direction: column;
}
.card__image {
aspect-ratio: 16 / 9;
object-fit: cover;
width: 100%;
}
.card__content {
padding: var(--space-md);
}
.card__title {
font-size: var(--font-size-heading);
line-height: 1.3;
}
/* 容器查询:宽屏模式 - 图文并排 */
@container card (min-width: 500px) {
.card__inner {
flex-direction: row;
}
.card__image {
width: 40%;
aspect-ratio: 1;
}
.card__content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
}
/* 容器查询:紧凑模式 - 隐藏次要信息 */
@container card (max-width: 250px) {
.card__description {
display: none;
}
.card__image {
aspect-ratio: 4 / 3;
}
}对应的 Tailwind CSS v4 写法:
<!-- 卡片网格 -->
<div class="grid grid-cols-[repeat(auto-fit,minmax(min(280px,100%),1fr))] gap-[clamp(0.75rem,0.5rem+0.5vw,1.5rem)] p-[clamp(1rem,0.75rem+1vw,2rem)]">
<!-- 单张卡片 - Container Query -->
<div class="@container rounded-lg shadow-sm overflow-hidden">
<div class="flex flex-col @[500px]:flex-row">
<img
src="/product.jpg"
alt="产品描述"
class="aspect-video @[500px]:aspect-square @[500px]:w-2/5 object-cover w-full"
/>
<div class="p-4 @[500px]:flex @[500px]:flex-col @[500px]:justify-center flex-1">
<h3 class="text-[clamp(1.25rem,1rem+0.75vw,1.75rem)] leading-tight">
产品标题
</h3>
<p class="mt-2 text-gray-600 @[..250px]:hidden">
产品描述文字...
</p>
</div>
</div>
</div>
</div>2. CSS 动画与过渡效果 AI 生成
2.1 动画技术分层
| 技术层级 | 技术 | 适用场景 | 性能影响 | AI 生成建议 |
|---|---|---|---|---|
| 第一层 | CSS transition | 悬停、焦点、状态切换 | 极低 | 优先使用,AI 生成质量高 |
| 第二层 | CSS @keyframes | 加载动画、循环动画、入场效果 | 低 | AI 可生成复杂关键帧 |
| 第三层 | @starting-style | 元素首次渲染的入场动画 | 低 | 新特性,需明确指导 AI |
| 第四层 | View Transitions API | 页面/路由切换动画 | 中 | 需要 JS 配合,AI 需上下文 |
| 第五层 | Web Animations API (WAAPI) | 复杂交互动画、时间线控制 | 中 | 需要详细需求描述 |
| 第六层 | Framer Motion / GSAP | 高级编排、物理动画、手势 | 中-高 | AI 熟悉度高,生成质量好 |
2.2 工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| AI CSS Animations | AI 生成 CSS 动画代码 | 免费 | 快速生成常见动画效果 |
| Workik CSS Animation Generator | AI 驱动的动画代码生成 | 免费 | 复杂关键帧和时序函数 |
| Framer Motion | React 动画库 | 免费(开源) | React 项目的声明式动画 |
| GSAP (GreenSock) | 专业动画引擎 | 免费 / Business $199/年 | 高性能复杂动画序列 |
| Motion One | 轻量级 Web Animations API 封装 | 免费(开源) | 性能敏感的简单动画 |
| Rive | 交互式动画设计工具 | 免费 / Team $25/月 | 设计师-开发者协作动画 |
| Lottie (Airbnb) | JSON 动画播放器 | 免费(开源) | After Effects 导出的矢量动画 |
| CSS Animation Generator (frontendtools.tech) | 可视化动画生成 | 免费 | 快速预览和调试动画参数 |
2.3 操作步骤:AI 生成动画效果
步骤 1:定义动画系统
在项目初期,用 AI 建立统一的动画系统,避免后期动画风格不一致:
提示词模板:
请为我的 [React/Vue] 项目设计一套统一的动画系统。
设计风格:[Material Design 3 / Apple HIG / 自定义]
性能要求:[60fps,避免布局抖动]
请提供:
1. 标准缓动函数集合(ease-in, ease-out, ease-in-out, spring)
- 使用 CSS 自定义属性定义
- 包含 cubic-bezier 值和语义化命名
2. 标准时长集合(fast: 150ms, normal: 300ms, slow: 500ms)
3. 常用动画预设:
- fade-in / fade-out
- slide-up / slide-down / slide-left / slide-right
- scale-in / scale-out
- skeleton 加载闪烁
4. 减少动画偏好支持(prefers-reduced-motion)
5. 所有动画只使用 transform 和 opacity(避免触发重排)步骤 2:生成具体动画效果
提示词模板——交互动画:
请为以下交互场景生成 CSS 动画代码:
场景:[下拉菜单展开 / 模态框弹出 / 通知条滑入 / 手风琴展开 / 标签页切换]
框架:[React + Tailwind CSS / Vue + 原生 CSS]
要求:
1. 使用 CSS transition 或 @keyframes(优先 transition)
2. 入场和退场动画都要有(不能只有入场)
3. 使用 @starting-style 实现从 display:none 到可见的过渡(如果浏览器支持)
4. 包含 prefers-reduced-motion 的降级处理
5. 动画只操作 transform 和 opacity
6. 提供 cubic-bezier 缓动函数(不要用默认 ease)
7. 确保动画不会阻塞用户交互提示词模板——页面过渡动画:
请为我的 [Next.js / Nuxt / SvelteKit] 应用实现页面过渡动画。
要求:
1. 使用 View Transitions API(带降级方案)
2. 页面切换时:旧页面淡出 + 新页面淡入
3. 共享元素过渡:列表页的卡片图片 → 详情页的 Hero 图片
4. 过渡时长 300ms,使用 ease-out 缓动
5. 提供不支持 View Transitions 的浏览器的降级方案
6. 尊重 prefers-reduced-motion 设置步骤 3:动画性能审查
提示词模板:
请审查以下动画代码的性能问题:
[粘贴动画代码]
检查项:
1. 是否有动画触发了布局重排(width, height, top, left, margin, padding)?
2. 是否所有动画都使用了 GPU 加速属性(transform, opacity)?
3. 是否使用了 will-change 提示(且没有过度使用)?
4. 是否有动画在滚动事件中触发(应使用 Intersection Observer)?
5. 是否尊重了 prefers-reduced-motion?
6. 动画时长是否合理(交互反馈 < 200ms,过渡 200-500ms)?
7. 是否有无限循环动画在不可见时仍在运行?2.4 动画代码示例
以下是一个 AI 生成的完整动画系统示例:
/* ===== 动画系统 ===== */
/* 1. 缓动函数 */
:root {
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.7, 0, 0.84, 0);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
/* 2. 时长 */
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
}
/* 3. 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* 4. 模态框动画 - 使用 @starting-style */
.modal-overlay {
opacity: 0;
transition: opacity var(--duration-normal) var(--ease-out),
display var(--duration-normal) allow-discrete;
display: none;
}
.modal-overlay[open] {
opacity: 1;
display: flex;
}
@starting-style {
.modal-overlay[open] {
opacity: 0;
}
}
.modal-content {
transform: scale(0.95) translateY(10px);
transition: transform var(--duration-normal) var(--ease-spring);
}
.modal-overlay[open] .modal-content {
transform: scale(1) translateY(0);
}
@starting-style {
.modal-overlay[open] .modal-content {
transform: scale(0.95) translateY(10px);
}
}
/* 5. 通知条滑入动画 */
@keyframes slide-in-right {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-enter {
animation: slide-in-right var(--duration-normal) var(--ease-out) forwards;
}
/* 6. 骨架屏闪烁 */
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.skeleton {
animation: skeleton-pulse 1.5s var(--ease-in-out) infinite;
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
background-size: 200% 100%;
border-radius: 4px;
}3. 无障碍(a11y)实现与 AI 辅助审查
3.1 2025-2026 无障碍合规形势
无障碍已从”最佳实践”变为”法律义务”:
| 法规/标准 | 生效时间 | 适用范围 | 要求 |
|---|---|---|---|
| WCAG 2.2 Level AA | 2023 年 10 月发布 | 全球通用标准 | 13 个等级 A-AAA 的成功标准 |
| 欧洲无障碍法案 (EAA) | 2025 年 6 月 28 日生效 | 欧盟成员国 | 所有数字产品和服务 |
| ADA Title II | 2026 年 4 月截止 | 美国政府机构 | 网站和移动应用 |
| WCAG 3.0 | 草案阶段(预计 2026+) | 下一代标准 | 新的评分模型和测试方法 |
关键数据: WebAIM 百万网站报告显示,排名前 100 万的网站中 96.3% 未通过基本无障碍测试。2024 年美国 ADA 相关诉讼达 4,605 起,同比增长 37%。
3.2 WCAG 2.2 核心要求速查
WCAG 2.2 基于四大原则(POUR):
| 原则 | 含义 | 前端开发者关注点 |
|---|---|---|
| Perceivable(可感知) | 信息和界面组件必须以用户可感知的方式呈现 | 替代文本、字幕、颜色对比度、文本缩放 |
| Operable(可操作) | 界面组件和导航必须可操作 | 键盘导航、触摸目标大小、时间限制、焦点管理 |
| Understandable(可理解) | 信息和界面操作必须可理解 | 语言标记、一致导航、错误提示、表单标签 |
| Robust(健壮) | 内容必须足够健壮以被各种用户代理解析 | 语义 HTML、ARIA 属性、有效标记 |
WCAG 2.2 新增标准(相比 2.1):
| 标准编号 | 名称 | 等级 | 前端实现要点 |
|---|---|---|---|
| 2.4.11 | Focus Not Obscured (Minimum) | AA | 焦点元素不被粘性头部/底部遮挡 |
| 2.4.12 | Focus Not Obscured (Enhanced) | AAA | 焦点元素完全可见 |
| 2.4.13 | Focus Appearance | AAA | 焦点指示器面积和对比度要求 |
| 2.5.7 | Dragging Movements | AA | 拖拽操作必须有替代方式 |
| 2.5.8 | Target Size (Minimum) | AA | 触摸/点击目标至少 24x24px |
| 3.2.6 | Consistent Help | A | 帮助机制在页面间位置一致 |
| 3.3.7 | Redundant Entry | A | 不要求用户重复输入已提供的信息 |
| 3.3.8 | Accessible Authentication (Minimum) | AA | 认证不依赖认知功能测试 |
| 3.3.9 | Accessible Authentication (Enhanced) | AAA | 认证不依赖任何认知测试 |
3.3 工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| axe-core (Deque) | 自动化无障碍测试引擎 | 免费(开源) | CI/CD 集成、单元测试 |
| axe DevTools 浏览器扩展 | 浏览器内无障碍审查 | 免费 / Pro $40/月 | 开发时实时检查 |
| axe MCP Server | AI 编码助手无障碍修复 | 免费 | 在 IDE 中一键修复 a11y 问题 |
| eslint-plugin-jsx-a11y | React JSX 静态无障碍检查 | 免费(开源) | 编码时即时反馈 |
| Pa11y | 命令行无障碍测试 | 免费(开源) | CI/CD 管线自动化测试 |
| Lighthouse (Google) | 综合网页质量审计 | 免费 | 快速评分和建议 |
| WAVE (WebAIM) | 在线无障碍评估 | 免费 | 可视化问题标注 |
| Polypane | 多视口浏览器 + a11y 检查 | $14/月起 | 响应式 + 无障碍一体化测试 |
| BrowserStack Accessibility | 云端自动化无障碍测试 | $29/月起 | 跨浏览器无障碍测试 |
| TestParty | AI 驱动的无障碍修复平台 | 联系销售 | 自动检测 + 代码修复 PR |
| Accessibility Insights (Microsoft) | 浏览器扩展 + 桌面工具 | 免费 | 引导式手动测试 |
| sa11y | 轻量级页面无障碍检查 | 免费(开源) | 内容编辑者自查 |
3.4 操作步骤:AI 辅助无障碍开发工作流
步骤 1:项目级无障碍基础设施
在项目启动时,用 AI 建立无障碍基础设施:
提示词模板:
请为我的 [React/Vue/Svelte] 项目建立无障碍基础设施。
项目信息:
- 框架:[Next.js 15 / Nuxt 4 / SvelteKit 2]
- UI 库:[shadcn/ui / Radix UI / Headless UI / 自建]
- CSS 方案:[Tailwind CSS v4 / CSS Modules]
请提供:
1. ESLint 无障碍规则配置(eslint-plugin-jsx-a11y 或等效插件)
2. 全局 a11y 工具函数:
- 焦点陷阱(Focus Trap)用于模态框
- 跳转到主内容链接(Skip to main content)
- 屏幕阅读器专用文本(visually-hidden 类)
- 实时区域公告(aria-live 封装)
3. 颜色对比度检查工具函数
4. 键盘导航 hook(useKeyboardNavigation)
5. axe-core 集成到测试框架的配置
6. prefers-reduced-motion 和 prefers-color-scheme 的响应式处理步骤 2:组件级无障碍实现
对每个交互组件,使用 AI 生成符合 ARIA 规范的代码:
提示词模板——表单无障碍:
请为以下表单组件添加完整的无障碍支持:
组件类型:[登录表单 / 搜索框 / 多步骤表单 / 日期选择器]
框架:[React + TypeScript]
无障碍要求:
1. 每个输入字段必须有关联的 <label>(使用 htmlFor/id 或嵌套)
2. 必填字段使用 aria-required="true"
3. 错误状态使用 aria-invalid="true" + aria-describedby 关联错误消息
4. 错误消息使用 aria-live="polite" 实时通知屏幕阅读器
5. 表单分组使用 <fieldset> + <legend>
6. 提交按钮在加载时使用 aria-busy="true" 和 aria-disabled="true"
7. 密码字段提供显示/隐藏切换,按钮有 aria-label
8. 自动完成建议列表使用 role="listbox" + role="option" + aria-activedescendant
9. 键盘导航:Tab 切换字段,Enter 提交,Escape 取消
10. 符合 WCAG 2.2 的 3.3.8 无障碍认证要求提示词模板——导航无障碍:
请为以下导航组件添加完整的无障碍支持:
组件类型:[响应式导航栏(桌面下拉 + 移动端汉堡菜单)]
框架:[React + Tailwind CSS]
无障碍要求:
1. 使用 <nav> 语义元素,添加 aria-label="主导航"
2. 当前页面链接使用 aria-current="page"
3. 下拉菜单:
- 触发按钮使用 aria-expanded 和 aria-haspopup="true"
- 菜单使用 role="menu",菜单项使用 role="menuitem"
- 键盘:Enter/Space 打开,Escape 关闭,方向键导航
4. 移动端汉堡菜单:
- 按钮有 aria-label="打开导航菜单" / "关闭导航菜单"
- 展开的菜单使用焦点陷阱
- 关闭时焦点返回触发按钮
5. 跳转链接:页面顶部有"跳转到主内容"链接
6. 焦点样式清晰可见(不使用 outline: none)提示词模板——数据表格无障碍:
请为以下数据表格添加完整的无障碍支持:
组件类型:[可排序、可分页的数据表格]
框架:[React + TypeScript]
无障碍要求:
1. 使用语义化 <table>、<thead>、<tbody>、<th>、<td>
2. <table> 添加 aria-label 或 <caption> 描述表格内容
3. 列标题 <th> 使用 scope="col",行标题使用 scope="row"
4. 可排序列:
- 排序按钮使用 aria-sort="ascending" / "descending" / "none"
- 排序变化后使用 aria-live 区域通知
5. 分页控件:
- 使用 <nav aria-label="分页导航">
- 当前页使用 aria-current="page"
- 页码按钮有 aria-label="第 N 页"
6. 空状态有明确的文字说明
7. 加载状态使用 aria-busy="true"
8. 行选择使用 checkbox + aria-label 描述选中内容步骤 3:自动化无障碍测试集成
将无障碍测试集成到开发和 CI/CD 流程中:
提示词模板:
请为我的项目配置自动化无障碍测试管线。
项目信息:
- 测试框架:[Vitest / Jest / Playwright / Cypress]
- CI/CD:[GitHub Actions / GitLab CI]
- 目标标准:WCAG 2.2 Level AA
请提供:
1. axe-core 集成到单元测试的配置
- 每个组件测试自动运行 axe 检查
- 自定义规则配置(禁用不适用的规则)
2. Playwright/Cypress E2E 测试中的 axe 集成
- 每个页面自动运行全页面 axe 扫描
- 生成 HTML 报告
3. CI/CD 管线配置
- PR 检查:axe 扫描失败则阻止合并
- 定期全站扫描(每周)
4. Pa11y CI 配置(作为补充检查)
5. Lighthouse CI 无障碍评分门槛(≥ 90 分)axe-core + Vitest 集成示例:
// tests/setup-a11y.ts
import { configureAxe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
// 自定义 axe 配置
export const axeConfig = configureAxe({
rules: {
// 根据项目需要调整规则
'color-contrast': { enabled: true },
'region': { enabled: true },
'landmark-one-main': { enabled: true },
},
});// components/Button.test.tsx
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import { Button } from './Button';
describe('Button 无障碍', () => {
it('不应有 axe 违规', async () => {
const { container } = render(
<Button onClick={() => {}}>提交</Button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('禁用状态应有正确的 ARIA 属性', () => {
const { getByRole } = render(
<Button disabled>提交</Button>
);
const button = getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
it('加载状态应通知屏幕阅读器', () => {
const { getByRole } = render(
<Button loading>提交</Button>
);
const button = getByRole('button');
expect(button).toHaveAttribute('aria-busy', 'true');
});
});GitHub Actions CI 配置示例:
# .github/workflows/a11y.yml
name: Accessibility Check
on:
pull_request:
branches: [main]
jobs:
a11y-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
# 单元测试中的 axe 检查
- name: Run a11y unit tests
run: npx vitest run --reporter=verbose tests/**/*.a11y.test.*
# Playwright E2E axe 扫描
- name: Run E2E a11y scan
run: npx playwright test --project=a11y
# Lighthouse CI
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
configPath: .lighthouserc.json
uploadArtifacts: true步骤 4:AI 辅助无障碍审查与修复
对已有代码进行无障碍审查和修复:
提示词模板:
请审查以下组件代码的无障碍问题,并提供修复方案:
[粘贴组件代码]
审查标准:WCAG 2.2 Level AA
请检查:
1. 语义 HTML:是否使用了正确的语义元素(而非 div 滥用)?
2. 替代文本:所有图片是否有有意义的 alt 属性?装饰性图片是否使用 alt=""?
3. 颜色对比度:文字与背景的对比度是否 ≥ 4.5:1(正文)/ 3:1(大文字)?
4. 键盘可访问:所有交互元素是否可通过键盘操作?
5. 焦点管理:焦点顺序是否合理?焦点样式是否可见?
6. ARIA 使用:ARIA 属性是否正确?是否有"ARIA 滥用"(能用原生 HTML 解决的不要用 ARIA)?
7. 表单标签:所有表单控件是否有关联的标签?
8. 动态内容:动态更新的内容是否使用了 aria-live?
9. 触摸目标:交互元素是否满足 24x24px 最小尺寸(WCAG 2.2 2.5.8)?
10. 焦点不被遮挡:焦点元素是否被粘性头部/底部遮挡(WCAG 2.2 2.4.11)?
对每个问题,请提供:
- 问题描述和 WCAG 标准编号
- 修复前的代码
- 修复后的代码
- 为什么这个修复是必要的3.5 Steering 规则:无障碍自动化保障
在项目的 Steering 规则文件中加入无障碍要求,让 AI 编码助手在生成代码时自动遵守:
CLAUDE.md / Kiro Steering 规则示例:
## 无障碍要求(强制)
所有前端代码必须遵守以下无障碍规则:
### HTML 语义
- 使用语义化 HTML 元素(nav, main, article, section, aside, header, footer)
- 禁止使用 div 模拟按钮、链接等交互元素
- 页面必须有且仅有一个 <main> 元素
- 标题层级必须连续(不跳过 h2 直接用 h3)
### 图片与媒体
- 所有 <img> 必须有 alt 属性
- 信息性图片的 alt 描述内容而非外观
- 装饰性图片使用 alt="" 和 role="presentation"
- 视频必须有字幕轨道
### 表单
- 每个表单控件必须有关联的 <label>
- 必填字段使用 aria-required="true"
- 错误消息使用 aria-describedby 关联到对应字段
- 表单验证错误使用 aria-live="polite" 通知
### 键盘与焦点
- 所有交互元素必须可通过键盘访问
- 焦点样式必须清晰可见(禁止 outline: none 不提供替代)
- 模态框必须实现焦点陷阱
- 关闭模态框后焦点必须返回触发元素
- 焦点元素不得被粘性元素遮挡
### 颜色与对比度
- 正文文字对比度 ≥ 4.5:1
- 大文字(18px+ 或 14px+ 粗体)对比度 ≥ 3:1
- 不依赖颜色作为唯一信息传达方式
### 动画
- 所有动画必须尊重 prefers-reduced-motion
- 禁止自动播放超过 5 秒的动画(除非可暂停)
- 闪烁频率不超过 3 次/秒
### 触摸目标
- 交互元素最小尺寸 24x24px(WCAG 2.2 AA)
- 推荐最小尺寸 44x44px(移动端最佳实践)
### ARIA 使用原则
- 第一规则:能用原生 HTML 解决的,不要用 ARIA
- 不改变原生语义(不要给 <h2> 加 role="button")
- 所有 ARIA 状态必须与实际状态同步
- 自定义组件必须遵循 WAI-ARIA Authoring Practices3.6 无障碍代码示例
以下是一个 AI 生成的无障碍模态框组件完整示例:
// components/AccessibleModal.tsx
import { useEffect, useRef, useCallback, type ReactNode } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
/** 关闭时焦点返回的元素 */
returnFocusRef?: React.RefObject<HTMLElement>;
}
export function AccessibleModal({
isOpen,
onClose,
title,
children,
returnFocusRef,
}: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const titleId = `modal-title-${title.replace(/\s+/g, '-').toLowerCase()}`;
// 焦点陷阱
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
return;
}
if (e.key !== 'Tab' || !modalRef.current) return;
const focusableElements = modalRef.current.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
},
[onClose]
);
useEffect(() => {
if (isOpen) {
// 打开时聚焦到模态框
const firstFocusable = modalRef.current?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
// 关闭时返回焦点
if (!isOpen) {
returnFocusRef?.current?.focus();
}
};
}, [isOpen, handleKeyDown, returnFocusRef]);
if (!isOpen) return null;
return (
{/* 遮罩层 */}
<div
className="fixed inset-0 z-50 flex items-center justify-center"
role="presentation"
>
{/* 背景遮罩 - 点击关闭 */}
<div
className="absolute inset-0 bg-black/50"
onClick={onClose}
aria-hidden="true"
/>
{/* 模态框内容 */}
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="relative z-10 w-full max-w-lg rounded-lg bg-white p-6 shadow-xl"
>
{/* 标题 */}
<h2 id={titleId} className="text-xl font-semibold">
{title}
</h2>
{/* 关闭按钮 */}
<button
onClick={onClose}
aria-label="关闭对话框"
className="absolute right-4 top-4 rounded-md p-2 hover:bg-gray-100
focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-blue-600"
>
<svg aria-hidden="true" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
{/* 内容 */}
<div className="mt-4">{children}</div>
</div>
</div>
);
}4. WCAG 合规检查工具链
4.1 检查工具分层策略
无障碍检查不能只依赖一个工具——自动化工具只能检测约 30-40% 的 WCAG 问题,其余需要半自动和手动测试配合。推荐采用三层检查策略:
┌─────────────────────────────────────────────────┐
│ 第三层:手动测试(覆盖 30-40%) │
│ 屏幕阅读器测试、键盘导航测试、认知负荷评估 │
├─────────────────────────────────────────────────┤
│ 第二层:半自动引导测试(覆盖 20-30%) │
│ Accessibility Insights 引导测试、WAVE 可视化标注 │
├─────────────────────────────────────────────────┤
│ 第一层:自动化扫描(覆盖 30-40%) │
│ axe-core、Lighthouse、Pa11y、eslint-plugin-jsx-a11y │
└─────────────────────────────────────────────────┘4.2 自动化检查工具配置
axe-core 集成方案
方案 A:开发时浏览器扩展
安装 axe DevTools 浏览器扩展(Chrome/Firefox/Edge),在 DevTools 中直接扫描当前页面。适合日常开发中的快速检查。
方案 B:单元测试集成
# React 项目
npm install --save-dev jest-axe @testing-library/react
# Vue 项目
npm install --save-dev vitest-axe @testing-library/vue方案 C:E2E 测试集成
# Playwright
npm install --save-dev @axe-core/playwright
# Cypress
npm install --save-dev cypress-axePlaywright + axe-core 示例:
// e2e/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('全站无障碍扫描', () => {
const pages = [
{ name: '首页', path: '/' },
{ name: '产品列表', path: '/products' },
{ name: '登录页', path: '/login' },
{ name: '注册页', path: '/register' },
{ name: '仪表板', path: '/dashboard' },
];
for (const page of pages) {
test(`${page.name} 应无 WCAG 2.2 AA 违规`, async ({ page: browserPage }) => {
await browserPage.goto(page.path);
const results = await new AxeBuilder({ page: browserPage })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
}
test('模态框打开状态的无障碍', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('[role="dialog"]');
const results = await new AxeBuilder({ page })
.include('[role="dialog"]')
.analyze();
expect(results.violations).toEqual([]);
});
});eslint-plugin-jsx-a11y 配置
// .eslintrc.json(React 项目)
{
"extends": [
"plugin:jsx-a11y/recommended"
],
"plugins": ["jsx-a11y"],
"rules": {
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-has-content": "error",
"jsx-a11y/aria-props": "error",
"jsx-a11y/aria-proptypes": "error",
"jsx-a11y/aria-role": "error",
"jsx-a11y/aria-unsupported-elements": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/heading-has-content": "error",
"jsx-a11y/html-has-lang": "error",
"jsx-a11y/img-redundant-alt": "warn",
"jsx-a11y/interactive-supports-focus": "error",
"jsx-a11y/label-has-associated-control": "error",
"jsx-a11y/no-access-key": "warn",
"jsx-a11y/no-autofocus": "warn",
"jsx-a11y/no-noninteractive-element-interactions": "warn",
"jsx-a11y/no-redundant-roles": "warn",
"jsx-a11y/role-has-required-aria-props": "error",
"jsx-a11y/role-supports-aria-props": "error",
"jsx-a11y/tabindex-no-positive": "error"
}
}Pa11y CI 配置
// .pa11yci.json
{
"defaults": {
"standard": "WCAG2AA",
"timeout": 30000,
"wait": 2000,
"chromeLaunchConfig": {
"args": ["--no-sandbox"]
},
"ignore": []
},
"urls": [
"http://localhost:3000/",
"http://localhost:3000/products",
"http://localhost:3000/login",
{
"url": "http://localhost:3000/dashboard",
"actions": [
"wait for element #main-content to be visible"
]
}
]
}4.3 手动测试清单
自动化工具无法覆盖的关键测试项:
| 测试类别 | 测试方法 | 检查要点 |
|---|---|---|
| 键盘导航 | 只用键盘操作整个页面 | Tab 顺序合理、所有功能可达、焦点可见、无键盘陷阱 |
| 屏幕阅读器 | 使用 VoiceOver (Mac) / NVDA (Windows) | 内容朗读顺序、交互元素描述、动态内容通知 |
| 缩放测试 | 浏览器缩放到 200% | 内容不溢出、不重叠、功能完整 |
| 颜色测试 | 使用灰度模式查看 | 信息不仅靠颜色传达 |
| 动画测试 | 开启”减少动画”系统设置 | 动画被禁用或简化 |
| 触摸测试 | 在真实移动设备上操作 | 触摸目标足够大、手势有替代方式 |
屏幕阅读器快速测试流程:
1. 打开 VoiceOver (Mac: Cmd+F5) 或 NVDA (Windows)
2. 使用 Tab 键遍历所有交互元素
✓ 每个元素是否有有意义的朗读内容?
✓ 按钮是否朗读了功能而非"按钮"?
✓ 链接是否朗读了目标而非"点击这里"?
3. 使用标题导航(VO+Cmd+H / NVDA+H)
✓ 标题层级是否连续?
✓ 标题是否描述了内容?
4. 使用地标导航(VO+Cmd+M / NVDA+D)
✓ 页面是否有 main、nav、header、footer 地标?
5. 测试表单填写
✓ 标签是否正确关联?
✓ 错误消息是否被朗读?
6. 测试动态内容
✓ 通知/提示是否被朗读?
✓ 加载状态是否有反馈?4.4 AI 辅助无障碍修复工作流
当发现无障碍问题时,使用 AI 快速修复:
提示词模板——批量修复:
以下是 axe-core 扫描发现的无障碍违规列表。请为每个违规提供修复代码:
违规列表:
[粘贴 axe-core 的 violations 输出]
对每个违规,请提供:
1. 违规说明(用中文)
2. 影响等级(critical / serious / moderate / minor)
3. 对应的 WCAG 标准编号
4. 修复前的代码片段
5. 修复后的代码片段
6. 修复原理说明
优先处理 critical 和 serious 级别的违规。提示词模板——axe MCP Server 集成:
我已安装 axe MCP Server。请扫描当前页面的无障碍问题,
并直接在代码中修复所有 critical 和 serious 级别的违规。
修复时请遵循以下原则:
1. 优先使用语义 HTML 而非 ARIA
2. 不改变视觉外观
3. 保持现有的组件 API 不变
4. 添加必要的注释说明修复原因5. 响应式 + 无障碍综合 Prompt 模板库
5.1 新组件生成模板
请生成一个 [组件名称] 组件,同时满足响应式和无障碍要求。
框架:[React + TypeScript + Tailwind CSS v4]
功能需求:
[描述组件功能]
响应式需求:
- 移动端 (<640px):[布局描述]
- 平板 (640px-1024px):[布局描述]
- 桌面 (>1024px):[布局描述]
- 使用 Container Queries 实现组件级响应式
无障碍需求:
- 符合 WCAG 2.2 Level AA
- 完整的键盘导航支持
- 屏幕阅读器友好的 ARIA 标记
- 颜色对比度 ≥ 4.5:1
- 触摸目标 ≥ 24x24px
- 尊重 prefers-reduced-motion
动画需求:
- [描述需要的动画效果]
- 使用 CSS transition/animation(优先)或 Framer Motion
- 只操作 transform 和 opacity
- prefers-reduced-motion 时禁用或简化
请同时提供:
1. 组件代码
2. 基本的 axe-core 无障碍测试
3. 使用示例5.2 现有组件无障碍改造模板
请对以下组件进行无障碍改造,使其符合 WCAG 2.2 Level AA:
[粘贴现有组件代码]
改造要求:
1. 不改变现有的视觉外观和功能
2. 添加必要的语义 HTML 和 ARIA 属性
3. 实现完整的键盘导航
4. 添加焦点管理(如果是模态/弹出类组件)
5. 确保颜色对比度达标
6. 添加 prefers-reduced-motion 支持
7. 确保触摸目标尺寸达标
请输出:
1. 改造后的完整组件代码
2. 改造说明(每处修改的原因和对应的 WCAG 标准)
3. 测试建议(需要手动验证的项目)5.3 响应式布局重构模板
请将以下使用固定布局的组件重构为现代响应式布局:
[粘贴现有代码]
重构目标:
1. 将固定像素宽度替换为相对单位和流体布局
2. 将 @media 查询替换为 @container 查询(适用的场景)
3. 使用 CSS Grid auto-fit/auto-fill 替代手动列数控制
4. 使用 clamp() 实现流体排版
5. 确保在 320px-2560px 范围内都可用
6. 图片使用 aspect-ratio + object-fit
7. 保持无障碍合规(触摸目标、焦点管理等)
请输出:
1. 重构后的代码
2. 重构前后的对比说明
3. 需要测试的断点列表5.4 动画系统生成模板
请为我的 [React/Vue/Svelte] 项目生成一套完整的动画工具集。
设计风格:[Material Design 3 / Apple HIG / 自定义柔和风格]
需要的动画类型:
1. 微交互:按钮悬停/按下、输入框聚焦、开关切换
2. 入场动画:列表项依次出现、卡片淡入、页面区块滑入
3. 过渡动画:标签页切换、手风琴展开/收起、侧边栏滑出
4. 反馈动画:成功/错误抖动、加载旋转、骨架屏闪烁
5. 页面过渡:路由切换的淡入淡出
技术要求:
- 使用 CSS 自定义属性定义缓动函数和时长
- 所有动画只操作 transform 和 opacity
- 完整的 prefers-reduced-motion 支持
- 提供 React hook(useAnimation)或 Vue composable
- 支持 Intersection Observer 触发的滚动动画
请输出:
1. CSS 动画变量和关键帧定义文件
2. 动画工具函数/hook
3. 使用示例
4. Storybook stories(可选)实战案例:电商产品列表页的响应式 + 无障碍全流程
案例背景
一个电商网站的产品列表页需要同时满足:
- 响应式布局(手机单列 → 平板双列 → 桌面三/四列)
- 完整的无障碍支持(键盘导航、屏幕阅读器、颜色对比度)
- 流畅的动画效果(卡片入场、悬停效果、筛选过渡)
- WCAG 2.2 Level AA 合规
步骤 1:使用 AI 生成响应式产品网格
Prompt:
请生成一个电商产品列表页的响应式网格组件。
框架:React + TypeScript + Tailwind CSS v4
布局需求:
- 移动端 (<640px):单列,卡片全宽
- 平板 (640px-1024px):两列网格
- 桌面 (1024px-1280px):三列网格
- 大屏 (>1280px):四列网格
- 使用 CSS Grid auto-fit 实现自适应
- 卡片最小宽度 280px
卡片内容:
- 产品图片(16:9 比例)
- 产品名称
- 价格(原价 + 折扣价)
- 评分(星级)
- "加入购物车"按钮
无障碍要求:
- 产品图片有描述性 alt 文本
- 价格使用 aria-label 提供完整描述(如"原价 299 元,现价 199 元")
- 评分使用 aria-label(如"评分 4.5 星,满分 5 星")
- 加入购物车按钮包含产品名称(如"将 iPhone 16 加入购物车")
- 卡片列表使用 role="list" + role="listitem"
- 键盘可导航,焦点样式清晰步骤 2:AI 生成的代码(关键片段)
// components/ProductGrid.tsx
interface Product {
id: string;
name: string;
image: string;
imageAlt: string;
originalPrice: number;
salePrice?: number;
rating: number;
reviewCount: number;
}
function ProductGrid({ products }: { products: Product[] }) {
return (
<section aria-label="产品列表">
<ul
role="list"
className="grid grid-cols-[repeat(auto-fit,minmax(min(280px,100%),1fr))] gap-6 p-4"
>
{products.map((product, index) => (
<li key={product.id} role="listitem">
<ProductCard product={product} index={index} />
</li>
))}
</ul>
{products.length === 0 && (
<p className="py-12 text-center text-gray-500" role="status">
没有找到匹配的产品
</p>
)}
</section>
);
}
function ProductCard({ product, index }: { product: Product; index: number }) {
const priceLabel = product.salePrice
? `原价 ${product.originalPrice} 元,现价 ${product.salePrice} 元`
: `${product.originalPrice} 元`;
const ratingLabel = `评分 ${product.rating} 星,满分 5 星,共 ${product.reviewCount} 条评价`;
return (
<article
className="@container group rounded-lg border border-gray-200 bg-white
shadow-sm transition-shadow duration-200 hover:shadow-md
focus-within:ring-2 focus-within:ring-blue-500"
style={{ animationDelay: `${index * 50}ms` }}
>
{/* 产品图片 */}
<a
href={`/products/${product.id}`}
className="block overflow-hidden rounded-t-lg"
aria-label={`查看 ${product.name} 详情`}
>
<img
src={product.image}
alt={product.imageAlt}
className="aspect-video w-full object-cover transition-transform
duration-300 group-hover:scale-105"
loading="lazy"
/>
</a>
{/* 产品信息 */}
<div className="p-4 @[400px]:p-6">
<h3 className="text-[clamp(0.875rem,0.8rem+0.25vw,1.125rem)] font-medium
leading-tight line-clamp-2">
<a
href={`/products/${product.id}`}
className="hover:text-blue-600 focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
{product.name}
</a>
</h3>
{/* 价格 */}
<div className="mt-2" aria-label={priceLabel}>
{product.salePrice ? (
<>
<span className="text-lg font-bold text-red-600">
¥{product.salePrice}
</span>
<span className="ml-2 text-sm text-gray-400 line-through"
aria-hidden="true">
¥{product.originalPrice}
</span>
</>
) : (
<span className="text-lg font-bold">¥{product.originalPrice}</span>
)}
</div>
{/* 评分 */}
<div className="mt-2 flex items-center" aria-label={ratingLabel}>
<div className="flex" aria-hidden="true">
{[1, 2, 3, 4, 5].map((star) => (
<svg
key={star}
className={`h-4 w-4 ${
star <= Math.round(product.rating)
? 'text-yellow-400'
: 'text-gray-300'
}`}
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
<span className="ml-1 text-sm text-gray-500" aria-hidden="true">
({product.reviewCount})
</span>
</div>
{/* 加入购物车按钮 */}
<button
className="mt-4 w-full rounded-md bg-blue-600 px-4 py-2.5
text-sm font-medium text-white
transition-colors duration-150
hover:bg-blue-700
focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-blue-600
active:bg-blue-800
disabled:cursor-not-allowed disabled:opacity-50"
aria-label={`将 ${product.name} 加入购物车`}
>
加入购物车
</button>
</div>
</article>
);
}步骤 3:添加入场动画(尊重 prefers-reduced-motion)
/* 产品卡片入场动画 */
@keyframes card-enter {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.product-card {
animation: card-enter 0.4s cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
/* 尊重用户偏好 */
@media (prefers-reduced-motion: reduce) {
.product-card {
animation: none;
opacity: 1;
}
}步骤 4:运行无障碍测试
// tests/ProductGrid.a11y.test.tsx
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import { ProductGrid } from './ProductGrid';
const mockProducts = [
{
id: '1',
name: '无线蓝牙耳机',
image: '/earbuds.jpg',
imageAlt: '白色无线蓝牙耳机,放在充电盒旁边',
originalPrice: 299,
salePrice: 199,
rating: 4.5,
reviewCount: 1234,
},
// ... 更多产品
];
describe('ProductGrid 无障碍测试', () => {
it('不应有 axe 违规', async () => {
const { container } = render(<ProductGrid products={mockProducts} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('空状态应有 role="status"', () => {
const { getByRole } = render(<ProductGrid products={[]} />);
expect(getByRole('status')).toHaveTextContent('没有找到匹配的产品');
});
it('加入购物车按钮应包含产品名称', () => {
const { getAllByRole } = render(<ProductGrid products={mockProducts} />);
const buttons = getAllByRole('button');
expect(buttons[0]).toHaveAttribute(
'aria-label',
'将 无线蓝牙耳机 加入购物车'
);
});
it('价格应有完整的 aria-label', () => {
const { container } = render(<ProductGrid products={mockProducts} />);
const priceElement = container.querySelector('[aria-label*="原价"]');
expect(priceElement).toHaveAttribute(
'aria-label',
'原价 299 元,现价 199 元'
);
});
});案例分析
关键决策点:
-
CSS Grid auto-fit vs 固定列数:使用
auto-fit+minmax()让网格自动适应容器宽度,无需为每个断点手动设置列数。这减少了代码量,也让组件在任何容器中都能正常工作。 -
aria-label vs 视觉文本:价格和评分的视觉呈现(删除线、星星图标)对屏幕阅读器不友好,因此使用
aria-label提供完整的文字描述,同时用aria-hidden="true"隐藏装饰性元素。 -
动画降级策略:使用
prefers-reduced-motion: reduce完全禁用入场动画,而非仅缩短时长。对于有前庭障碍的用户,任何移动都可能引起不适。 -
焦点管理:使用
focus-visible而非focus,确保只在键盘导航时显示焦点环,鼠标点击时不显示,兼顾视觉美观和无障碍。 -
Container Query 预留:卡片使用
@container类,为未来在不同布局上下文(侧边栏、模态框)中复用做好准备。
避坑指南
❌ 常见错误
-
只用
outline: none去掉焦点环,不提供替代样式- 问题:键盘用户完全无法看到当前焦点位置,违反 WCAG 2.4.7
- 正确做法:使用
focus-visible提供清晰的焦点样式,或自定义焦点环样式
/* ❌ 错误 */ button:focus { outline: none; } /* ✅ 正确 */ button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; } -
用
div+onClick模拟按钮- 问题:div 没有原生的键盘支持(Enter/Space 触发)、没有 role、不在 Tab 序列中
- 正确做法:使用原生
<button>元素。如果必须用 div,需要添加role="button"、tabindex="0"、键盘事件处理
{/* ❌ 错误 */} <div onClick={handleClick}>点击我</div> {/* ✅ 正确 */} <button onClick={handleClick}>点击我</button> -
图片 alt 文本写”图片”或文件名
- 问题:屏幕阅读器会朗读”图片 IMG_20240101.jpg”,毫无意义
- 正确做法:描述图片内容和功能。装饰性图片使用
alt=""
{/* ❌ 错误 */} <img src="/hero.jpg" alt="图片" /> <img src="/hero.jpg" alt="hero-banner.jpg" /> {/* ✅ 正确 */} <img src="/hero.jpg" alt="团队成员在办公室讨论项目方案" /> <img src="/decorative-line.svg" alt="" role="presentation" /> -
响应式布局只测试了 3 个断点
- 问题:在断点之间的”中间地带”可能出现布局错乱(文字溢出、元素重叠)
- 正确做法:使用 Responsively App 或 Polypane 进行连续宽度测试,从 320px 到 2560px 拖拽调整
-
动画不尊重
prefers-reduced-motion- 问题:有前庭障碍的用户可能因动画感到眩晕或恶心
- 正确做法:在全局样式中添加
prefers-reduced-motion媒体查询,禁用或简化所有动画
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } -
颜色对比度不达标却不自知
- 问题:浅灰色文字在白色背景上看起来”优雅”,但对比度可能只有 2:1(要求 4.5:1)
- 正确做法:使用 axe DevTools 或 Polypane 的对比度检查器验证每个文字颜色组合
/* ❌ 对比度 2.3:1 */ color: #b0b0b0; background: #ffffff; /* ✅ 对比度 4.6:1 */ color: #6b7280; background: #ffffff; -
Container Query 忘记设置
container-type- 问题:
@container查询不生效,因为没有声明容器 - 正确做法:在父元素上设置
container-type: inline-size
/* ❌ 忘记声明容器 */ .card { /* 没有 container-type */ } @container (min-width: 500px) { /* 不生效 */ } /* ✅ 正确声明 */ .card { container-type: inline-size; } @container (min-width: 500px) { /* 正常工作 */ } - 问题:
-
模态框没有焦点陷阱
- 问题:打开模态框后,Tab 键可以跳到模态框后面的页面元素,键盘用户迷失方向
- 正确做法:实现焦点陷阱(Tab 循环在模态框内),Escape 关闭,关闭后焦点返回触发元素
-
动画使用
width/height/top/left而非transform- 问题:触发浏览器重排(reflow),导致动画卡顿,尤其在移动设备上
- 正确做法:只使用
transform(translate, scale, rotate)和opacity做动画
/* ❌ 触发重排 */ .slide-in { left: -100%; transition: left 0.3s; } .slide-in.active { left: 0; } /* ✅ GPU 加速 */ .slide-in { transform: translateX(-100%); transition: transform 0.3s; } .slide-in.active { transform: translateX(0); } -
ARIA 滥用——能用原生 HTML 解决的用了 ARIA
- 问题:ARIA 增加了复杂性,且容易出错。“ARIA 的第一规则是不要使用 ARIA”
- 正确做法:优先使用语义化 HTML 元素
{/* ❌ ARIA 滥用 */} <div role="button" tabIndex={0} onClick={handleClick}>提交</div> <div role="navigation"><div role="list">...</div></div> {/* ✅ 语义 HTML */} <button onClick={handleClick}>提交</button> <nav><ul>...</ul></nav>
✅ 最佳实践
-
“无障碍优先”开发:在组件设计阶段就考虑无障碍,而非开发完成后补救。使用 Steering 规则强制 AI 生成无障碍代码。
-
建立无障碍组件库:使用 Radix UI、Headless UI 或 React Aria 等无障碍优先的无头组件库作为基础,在此之上添加样式。
-
自动化 + 手动双轨测试:CI/CD 中集成 axe-core 自动扫描,每个 Sprint 安排一次屏幕阅读器手动测试。
-
流体优先的响应式策略:优先使用
clamp()、auto-fit、flex-wrap等内在响应式技术,减少对断点的依赖。 -
动画性能预算:设定动画性能预算(如:同时运行的动画不超过 3 个,总动画时长不超过 500ms),在 Steering 规则中强制执行。
-
使用 axe MCP Server:在 IDE 中集成 Deque 的 axe MCP Server,让 AI 编码助手在生成代码时自动检查和修复无障碍问题。
-
Container Query 优先:新组件优先使用 Container Queries 实现响应式,让组件真正可复用、布局无关。
-
定期全站无障碍审计:每季度使用 BrowserStack Accessibility 或 Pa11y CI 进行全站扫描,跟踪无障碍评分趋势。
相关资源与延伸阅读
工具与平台
- axe-core (GitHub) — Deque 开源的无障碍测试引擎,被 Google Lighthouse 和数千个测试程序使用
- axe MCP Server — Deque 推出的 MCP Server,让 AI 编码助手直接在 IDE 中修复无障碍问题
- Responsively App — 开源的多设备同步预览浏览器,响应式开发必备
- Polypane — 专业的多视口浏览器,集成无障碍检查和响应式测试
规范与指南
- WCAG 2.2 规范 — W3C 官方 Web 内容无障碍指南 2.2 版本
- WAI-ARIA Authoring Practices — W3C 官方 ARIA 设计模式和组件实现指南
- CSS Container Queries 规范 — W3C 官方 CSS 容器查询规范
学习资源
- WebAIM — 犹他州立大学的 Web 无障碍项目,提供培训、评估工具和年度百万网站报告
- A11y Project — 社区驱动的无障碍知识库,包含清单、资源和最佳实践
- Josh W. Comeau - Container Queries Unleashed — 深入浅出的 Container Queries 教程
组件库
- Radix UI — 无障碍优先的无头 React 组件库
- React Aria (Adobe) — Adobe 出品的无障碍 React hooks 库
参考来源
- Web Accessibility in 2026: The Frontend Developer’s Survival Guide (2025-01)
- Container Queries in 2026: Powerful, but not a silver bullet (2025-12)
- WCAG 2.2 Compliance Checklist: Complete 2025 Implementation Roadmap (2025-11)
- 10 AI-Powered WCAG Tools That Actually Fix Accessibility Issues (2025-07)
- Automating Accessibility Testing in 2026 (2025-12)
- CSS in 2025: New Selectors, Container Queries, and AI-Generated Styles (2025-02)
- ARIA Labels for Web Accessibility: Complete 2025 Implementation Guide (2025-01)
- Deque vs Pa11y vs AccessLint: Automated Testing 2025 (2025-05)
- Component-First Responsive Design with Container Queries and Tailwind v4 (2025-12)
- 7 Best AI Tools for CSS Web Development in 2026 (2025-12)
- Automated Accessibility Audit AI Tools 2026 (2026-01)
- WCAG 2.2 Front-End Checklist for Developers (2025-05)
📖 返回 总览与导航 | 上一节:27d-设计系统与组件库维护 | 下一节:27f-前端Steering规则与反模式