Skip to Content

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.devAI 生成响应式 UI 组件免费(有限额)/ Pro $20/月快速原型、Tailwind 组件
CursorAI 编码助手,支持响应式代码生成免费 / Pro $20/月日常开发、代码补全
Claude CodeAgentic 编码,理解项目上下文按 token 计费复杂布局重构、全项目响应式改造
KiroSpec-Driven 开发,Steering 规则免费(预览期)规范化响应式开发流程
WindframeTailwind CSS 可视化构建器免费 / Pro $12/月可视化拖拽响应式布局
WorkikAI 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 AnimationsAI 生成 CSS 动画代码免费快速生成常见动画效果
Workik CSS Animation GeneratorAI 驱动的动画代码生成免费复杂关键帧和时序函数
Framer MotionReact 动画库免费(开源)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 AA2023 年 10 月发布全球通用标准13 个等级 A-AAA 的成功标准
欧洲无障碍法案 (EAA)2025 年 6 月 28 日生效欧盟成员国所有数字产品和服务
ADA Title II2026 年 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.11Focus Not Obscured (Minimum)AA焦点元素不被粘性头部/底部遮挡
2.4.12Focus Not Obscured (Enhanced)AAA焦点元素完全可见
2.4.13Focus AppearanceAAA焦点指示器面积和对比度要求
2.5.7Dragging MovementsAA拖拽操作必须有替代方式
2.5.8Target Size (Minimum)AA触摸/点击目标至少 24x24px
3.2.6Consistent HelpA帮助机制在页面间位置一致
3.3.7Redundant EntryA不要求用户重复输入已提供的信息
3.3.8Accessible Authentication (Minimum)AA认证不依赖认知功能测试
3.3.9Accessible Authentication (Enhanced)AAA认证不依赖任何认知测试

3.3 工具推荐

工具用途价格适用场景
axe-core (Deque)自动化无障碍测试引擎免费(开源)CI/CD 集成、单元测试
axe DevTools 浏览器扩展浏览器内无障碍审查免费 / Pro $40/月开发时实时检查
axe MCP ServerAI 编码助手无障碍修复免费在 IDE 中一键修复 a11y 问题
eslint-plugin-jsx-a11yReact JSX 静态无障碍检查免费(开源)编码时即时反馈
Pa11y命令行无障碍测试免费(开源)CI/CD 管线自动化测试
Lighthouse (Google)综合网页质量审计免费快速评分和建议
WAVE (WebAIM)在线无障碍评估免费可视化问题标注
Polypane多视口浏览器 + a11y 检查$14/月起响应式 + 无障碍一体化测试
BrowserStack Accessibility云端自动化无障碍测试$29/月起跨浏览器无障碍测试
TestPartyAI 驱动的无障碍修复平台联系销售自动检测 + 代码修复 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 Practices

3.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-axe

Playwright + 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 元' ); }); });

案例分析

关键决策点:

  1. CSS Grid auto-fit vs 固定列数:使用 auto-fit + minmax() 让网格自动适应容器宽度,无需为每个断点手动设置列数。这减少了代码量,也让组件在任何容器中都能正常工作。

  2. aria-label vs 视觉文本:价格和评分的视觉呈现(删除线、星星图标)对屏幕阅读器不友好,因此使用 aria-label 提供完整的文字描述,同时用 aria-hidden="true" 隐藏装饰性元素。

  3. 动画降级策略:使用 prefers-reduced-motion: reduce 完全禁用入场动画,而非仅缩短时长。对于有前庭障碍的用户,任何移动都可能引起不适。

  4. 焦点管理:使用 focus-visible 而非 focus,确保只在键盘导航时显示焦点环,鼠标点击时不显示,兼顾视觉美观和无障碍。

  5. Container Query 预留:卡片使用 @container 类,为未来在不同布局上下文(侧边栏、模态框)中复用做好准备。


避坑指南

❌ 常见错误

  1. 只用 outline: none 去掉焦点环,不提供替代样式

    • 问题:键盘用户完全无法看到当前焦点位置,违反 WCAG 2.4.7
    • 正确做法:使用 focus-visible 提供清晰的焦点样式,或自定义焦点环样式
    /* ❌ 错误 */ button:focus { outline: none; } /* ✅ 正确 */ button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
  2. div + onClick 模拟按钮

    • 问题:div 没有原生的键盘支持(Enter/Space 触发)、没有 role、不在 Tab 序列中
    • 正确做法:使用原生 <button> 元素。如果必须用 div,需要添加 role="button"tabindex="0"、键盘事件处理
    {/* ❌ 错误 */} <div onClick={handleClick}>点击我</div> {/* ✅ 正确 */} <button onClick={handleClick}>点击我</button>
  3. 图片 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" />
  4. 响应式布局只测试了 3 个断点

    • 问题:在断点之间的”中间地带”可能出现布局错乱(文字溢出、元素重叠)
    • 正确做法:使用 Responsively App 或 Polypane 进行连续宽度测试,从 320px 到 2560px 拖拽调整
  5. 动画不尊重 prefers-reduced-motion

    • 问题:有前庭障碍的用户可能因动画感到眩晕或恶心
    • 正确做法:在全局样式中添加 prefers-reduced-motion 媒体查询,禁用或简化所有动画
    @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
  6. 颜色对比度不达标却不自知

    • 问题:浅灰色文字在白色背景上看起来”优雅”,但对比度可能只有 2:1(要求 4.5:1)
    • 正确做法:使用 axe DevTools 或 Polypane 的对比度检查器验证每个文字颜色组合
    /* ❌ 对比度 2.3:1 */ color: #b0b0b0; background: #ffffff; /* ✅ 对比度 4.6:1 */ color: #6b7280; background: #ffffff;
  7. 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) { /* 正常工作 */ }
  8. 模态框没有焦点陷阱

    • 问题:打开模态框后,Tab 键可以跳到模态框后面的页面元素,键盘用户迷失方向
    • 正确做法:实现焦点陷阱(Tab 循环在模态框内),Escape 关闭,关闭后焦点返回触发元素
  9. 动画使用 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); }
  10. 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>

✅ 最佳实践

  1. “无障碍优先”开发:在组件设计阶段就考虑无障碍,而非开发完成后补救。使用 Steering 规则强制 AI 生成无障碍代码。

  2. 建立无障碍组件库:使用 Radix UI、Headless UI 或 React Aria 等无障碍优先的无头组件库作为基础,在此之上添加样式。

  3. 自动化 + 手动双轨测试:CI/CD 中集成 axe-core 自动扫描,每个 Sprint 安排一次屏幕阅读器手动测试。

  4. 流体优先的响应式策略:优先使用 clamp()auto-fitflex-wrap 等内在响应式技术,减少对断点的依赖。

  5. 动画性能预算:设定动画性能预算(如:同时运行的动画不超过 3 个,总动画时长不超过 500ms),在 Steering 规则中强制执行。

  6. 使用 axe MCP Server:在 IDE 中集成 Deque 的 axe MCP Server,让 AI 编码助手在生成代码时自动检查和修复无障碍问题。

  7. Container Query 优先:新组件优先使用 Container Queries 实现响应式,让组件真正可复用、布局无关。

  8. 定期全站无障碍审计:每季度使用 BrowserStack Accessibility 或 Pa11y CI 进行全站扫描,跟踪无障碍评分趋势。


相关资源与延伸阅读

工具与平台

  1. axe-core (GitHub)  — Deque 开源的无障碍测试引擎,被 Google Lighthouse 和数千个测试程序使用
  2. axe MCP Server  — Deque 推出的 MCP Server,让 AI 编码助手直接在 IDE 中修复无障碍问题
  3. Responsively App  — 开源的多设备同步预览浏览器,响应式开发必备
  4. Polypane  — 专业的多视口浏览器,集成无障碍检查和响应式测试

规范与指南

  1. WCAG 2.2 规范  — W3C 官方 Web 内容无障碍指南 2.2 版本
  2. WAI-ARIA Authoring Practices  — W3C 官方 ARIA 设计模式和组件实现指南
  3. CSS Container Queries 规范  — W3C 官方 CSS 容器查询规范

学习资源

  1. WebAIM  — 犹他州立大学的 Web 无障碍项目,提供培训、评估工具和年度百万网站报告
  2. A11y Project  — 社区驱动的无障碍知识库,包含清单、资源和最佳实践
  3. Josh W. Comeau - Container Queries Unleashed  — 深入浅出的 Container Queries 教程

组件库

  1. Radix UI  — 无障碍优先的无头 React 组件库
  2. React Aria (Adobe)  — Adobe 出品的无障碍 React hooks 库

参考来源


📖 返回 总览与导航 | 上一节:27d-设计系统与组件库维护 | 下一节:27f-前端Steering规则与反模式

Last updated on