Loading...
Loading...
Compare original and translation side by side
/deer-sensechameleon-adapt/deer-sensechameleon-adaptLISTEN → SCAN → TEST → GUIDE → PROTECT
↓ ↲ ↲ ↓ ↓
Hear Look for Validate Teach Ensure
Needs Barriers Paths Good InclusionLISTEN → SCAN → TEST → GUIDE → PROTECT
↓ ↲ ↲ ↓ ↓
聆听需求 排查障碍 验证路径 引导优化 保障包容性| Disability Type | Assistive Technology | What They Need |
|---|---|---|
| Visual | Screen readers, magnification | Alt text, semantic HTML, focus indicators |
| Motor | Keyboard, switch controls | Keyboard navigation, large touch targets |
| Cognitive | Simplified interfaces | Clear language, consistent patterns |
| Auditory | Captions, visual indicators | Transcripts, visual alerts |
Visual Barriers:
- Poor color contrast
- Missing alt text
- Keyboard traps
- No focus indicators
Motor Barriers:
- Small touch targets (<44px)
- No keyboard support
- Time limits too short
- Required precision
Cognitive Barriers:
- Confusing navigation
- Missing error explanations
- Inconsistent patterns
- Too much information at once| 障碍类型 | 辅助技术 | 需求要点 |
|---|---|---|
| 视觉障碍 | 屏幕阅读器、放大镜 | 替代文本(Alt text)、语义化HTML、焦点指示器 |
| 肢体障碍 | 键盘、切换控制设备 | 键盘导航、大尺寸触控目标 |
| 认知障碍 | 简化界面 | 清晰语言、一致交互模式 |
| 听觉障碍 | 字幕、视觉指示器 | 文字转录、视觉提醒 |
视觉障碍:
- 色彩对比度不足
- 缺少替代文本
- 键盘陷阱(无法Tab离开)
- 无焦点指示器
肢体障碍:
- 触控目标过小(<44px)
- 无键盘支持
- 时间限制过短
- 操作精度要求过高
认知障碍:
- 导航逻辑混乱
- 错误提示不清晰
- 交互模式不一致
- 信息过载undefinedundefined
**Lighthouse CI:**
```bash
**Lighthouse CI:**
```bash
**Svelte Accessibility Warnings:**
```svelte
<!-- Svelte warns about common issues -->
<!-- ❌ Missing alt text -->
<img src="photo.jpg">
<!-- ✅ Descriptive alt -->
<img src="photo.jpg" alt="Sunset over the grove">
<!-- ❌ No label -->
<input type="text">
<!-- ✅ Associated label -->
<label for="email">Email</label>
<input id="email" type="text">
<!-- ❌ Click without keyboard -->
<div onclick={handleClick}>
<!-- ✅ Button with keyboard -->
<button onclick={handleClick}>// a11y-checklist.ts
const checks = {
// Images
imagesHaveAlt: 'All <img> have descriptive alt text',
decorativeMarked: 'Decorative images have alt=""',
// Forms
labelsPresent: 'All inputs have associated labels',
errorsClear: 'Error messages explain how to fix',
requiredMarked: 'Required fields are indicated',
// Navigation
focusVisible: 'Focus indicators are visible',
skipLinks: 'Skip navigation links present',
headingOrder: 'Headings follow logical order (h1→h2→h3)',
// Color
contrastAA: 'Text contrast ratio >= 4.5:1',
contrastLarge: 'Large text contrast >= 3:1',
notColorOnly: 'Information not conveyed by color alone',
// Structure
landmarks: 'Page has main, nav, complementary landmarks',
langAttribute: '<html lang="en"> set correctly',
titlePresent: 'Each page has unique <title>'
};
**Svelte 无障碍警告:**
```svelte
<!-- Svelte 会警告常见问题 -->
<!-- ❌ 缺少替代文本 -->
<img src="photo.jpg">
<!-- ✅ 描述性替代文本 -->
<img src="photo.jpg" alt="林间日落">
<!-- ❌ 无标签 -->
<input type="text">
<!-- ✅ 关联标签 -->
<label for="email">邮箱</label>
<input id="email" type="text">
<!-- ❌ 仅支持点击的元素无键盘交互 -->
<div onclick={handleClick}>
<!-- ✅ 支持键盘交互的按钮 -->
<button onclick={handleClick}>// a11y-checklist.ts
const checks = {
// 图片
imagesHaveAlt: '所有<img>标签包含描述性替代文本',
decorativeMarked: '装饰性图片设置alt=""',
// 表单
labelsPresent: '所有输入框关联对应标签',
errorsClear: '错误提示说明修复方法',
requiredMarked: '必填字段有明确标识',
// 导航
focusVisible: '焦点指示器可见',
skipLinks: '提供跳过导航的快捷链接',
headingOrder: '标题遵循逻辑层级(h1→h2→h3)',
// 色彩
contrastAA: '文本对比度≥4.5:1',
contrastLarge: '大文本对比度≥3:1',
notColorOnly: '信息不单独通过色彩传递',
// 结构
landmarks: '页面包含main、nav、complementary等地标元素',
langAttribute: '<html lang="en">设置正确',
titlePresent: '每个页面有唯一的<title>'
};Test Checklist:
□ Tab through entire page - does order make sense?
□ Can you activate every button/link with Enter/Space?
□ Is there a visible focus indicator on everything?
□ Can you escape modals with Escape key?
□ No keyboard traps (can't Tab away)?
□ Skip link works (jumps past navigation)?undefined测试清单:
□ 按Tab键遍历整个页面 - 顺序是否符合逻辑?
□ 能否用Enter/Space激活所有按钮/链接?
□ 所有可交互元素的焦点指示器是否可见?
□ 能否用Escape键关闭弹窗?
□ 无键盘陷阱(无法Tab离开)?
□ 跳过导航的快捷链接是否可用?undefined
**Screen Reader Checklist:**
**Reduced Motion:**
```svelte
<script>
import { reducedMotion } from '$lib/stores/accessibility';
</script>
{#if !$reducedMotion}
<FallingLeavesLayer />
{:else}
<!-- Static version for reduced motion -->
<StaticLeaves />
{/if}Test at:
□ 100% (normal)
□ 150% (mild vision loss)
□ 200% (moderate vision loss)
□ 400% (severe vision loss)
Verify:
- No horizontal scrolling at 200%
- All content still visible
- No overlapping elementsSetup: Multi-step registration form with conditional fields
1. Tab to email field
✓ Focus indicator visible?
✓ Label announced by screen reader?
2. Enter invalid email, Tab away
✓ Error message announced live?
✓ Error associated with field (aria-describedby)?
3. Check "Business account" checkbox
✓ New company name field appears
✓ Focus moves to new field OR stays logical?
✓ Screen reader announces new content?
4. Tab through to submit
✓ Skip link available to jump sections?
✓ Submit button clearly labeled?
✓ Can activate with Enter AND Space?Setup: Page using GroveTerm/GroveSwap/GroveText for terminology
1. Tab to a GroveTerm element
✓ Focus ring visible (2px outline)?
✓ aria-label reads "Grove term: [term], [category] category"?
✓ role="button" and tabindex="0" present?
2. Activate with Enter or Space
✓ Popup opens with definition?
✓ Focus moves to popup content?
✓ ESC closes popup and returns focus?
3. Toggle Grove Mode OFF
✓ Standard terms display (e.g., "Posts" not "Blooms")?
✓ Screen reader announces "Grove's name for [standard term]" in popup?
✓ GroveSwap silently updates without focus disruption?
4. Check prefers-reduced-motion
✓ No animations on GroveTerm hover/open?
✓ Popup appears instantly without transition?
5. GroveText with [[term]] syntax
✓ Parsed terms are keyboard-navigable?
✓ Each rendered GroveTerm has correct aria attributes?Setup: Posts table with sortable columns
1. Navigate to table
✓ Table has caption or aria-label?
✓ Column headers use <th scope="col">?
✓ Row headers use <th scope="row">?
2. Tab to "Sort by date" column header
✓ Sort button focusable?
✓ Current sort direction announced (aria-sort)?
3. Activate sort with Enter
✓ Table updates without losing focus?
✓ Sort change announced (aria-live)?
✓ New sort direction reflected?
4. Navigate table with arrow keys (screen reader)
✓ Can move cell-by-cell?
✓ Row/column context announced?Setup: "Delete post" confirmation modal
1. Trigger modal from button
✓ Focus moves INTO modal?
✓ First focusable element receives focus?
✓ Modal has role="dialog" and aria-modal="true"?
✓ Modal has accessible name (aria-labelledby)?
2. Tab through modal
✓ Focus trapped inside modal?
✓ Tab wraps from last to first element?
✓ Shift+Tab works backwards?
3. Press Escape
✓ Modal closes?
✓ Focus returns to trigger button?
4. Click overlay/backdrop
✓ Modal closes (if intended behavior)?
✓ Or click does nothing (if modal is critical)?
**屏幕阅读器测试清单:**
**减少动画测试:**
```svelte
<script>
import { reducedMotion } from '$lib/stores/accessibility';
</script>
{#if !$reducedMotion}
<FallingLeavesLayer />
{:else}
<!-- 静态版本适配减少动画需求 -->
<StaticLeaves />
{/if}测试缩放比例:
□ 100%(正常)
□ 150%(轻度视力障碍)
□ 200%(中度视力障碍)
□ 400%(重度视力障碍)
验证内容:
- 200%缩放时无横向滚动
- 所有内容仍可见
- 无元素重叠场景:多步骤注册表单,包含条件显示字段
1. Tab切换到邮箱输入框
✓ 焦点指示器可见?
✓ 屏幕阅读器会播报标签?
2. 输入无效邮箱,按Tab离开
✓ 错误提示会实时播报?
✓ 错误提示与输入框关联(aria-describedby)?
3. 勾选「企业账户」复选框
✓ 新的公司名称字段显示?
✓ 焦点移动到新字段或保持逻辑顺序?
✓ 屏幕阅读器播报新增内容?
4. Tab切换到提交按钮
✓ 提供跳过章节的快捷链接?
✓ 提交按钮标签清晰?
✓ 可用Enter和Space键激活?场景:使用GroveTerm/GroveSwap/GroveTerminology的页面
1. Tab切换到GroveTerm元素
✓ 焦点环可见(2px轮廓)?
✓ aria-label显示「Grove术语: [术语名], [分类]」?
✓ 包含role="button"和tabindex="0"属性?
2. 用Enter或Space激活
✓ 弹出定义窗口?
✓ 焦点移动到弹窗内容?
✓ 按ESC关闭弹窗并返回原焦点?
3. 关闭Grove模式
✓ 显示标准术语(如「Posts」而非「Blooms」)?
✓ 屏幕阅读器在弹窗中播报「Grove对[标准术语]的命名」?
✓ GroveSwap静默更新且不干扰焦点?
4. 检查prefers-reduced-motion
✓ GroveTerm hover/打开时无动画?
✓ 弹窗直接显示无过渡效果?
5. 带[[term]]语法的GroveText
✓ 解析后的术语支持键盘导航?
✓ 每个渲染的GroveTerm属性正确?场景:可排序的文章列表表格
1. 导航到表格
✓ 表格有标题或aria-label?
✓ 列头使用<th scope="col">?
✓ 行头使用<th scope="row">?
2. Tab切换到「按日期排序」列头
✓ 排序按钮可获取焦点?
✓ 播报当前排序方向(aria-sort)?
3. 按Enter激活排序
✓ 表格更新时不丢失焦点?
✓ 排序变更被实时播报(aria-live)?
✓ 反映新的排序方向?
4. 用箭头键导航表格(屏幕阅读器)
✓ 可逐单元格移动?
✓ 播报行/列上下文?场景:「删除文章」确认弹窗
1. 点击按钮触发弹窗
✓ 焦点移动到弹窗内?
✓ 第一个可交互元素获取焦点?
✓ 弹窗设置role="dialog"和aria-modal="true"?
✓ 弹窗有可访问名称(aria-labelledby)?
2. Tab遍历弹窗
✓ 焦点被限制在弹窗内?
✓ Tab从最后一个元素循环到第一个?
✓ Shift+Tab可反向遍历?
3. 按Escape键
✓ 弹窗关闭?
✓ 焦点返回触发按钮?
4. 点击遮罩层
✓ 弹窗关闭(若为预期行为)?
✓ 或点击无响应(若弹窗为关键操作)?<!-- Check contrast ratios -->
<!-- Use WebAIM contrast checker or similar -->
<!-- Bad: Light gray on white (fails) -->
<p class="text-gray-400">Subtle text</p>
<!-- Good: Darker gray (passes AA) -->
<p class="text-gray-600">Readable text</p>
<!-- Use the palette (already tested for contrast) -->
<p class="text-greens-grove">Brand text</p><!-- ❌ Div soup -->
<div class="card" onclick={handleClick}>
<div class="title">Heading</div>
<div class="text">Content</div>
</div>
<!-- ✅ Proper semantics -->
<article class="card">
<h2>Heading</h2>
<p>Content</p>
<button onclick={handleClick}>Action</button>
</article><script>
let modalOpen = $state(false);
let modalRef: HTMLDivElement;
let previousFocus: Element;
function openModal() {
previousFocus = document.activeElement;
modalOpen = true;
// Focus first input when modal opens
tick().then(() => {
modalRef?.querySelector('input')?.focus();
});
}
function closeModal() {
modalOpen = false;
// Return focus to trigger button
previousFocus?.focus();
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') closeModal();
}
</script>
{#if modalOpen}
<div
bind:this={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onkeydown={handleKeydown}
>
<h2 id="modal-title">Modal Title</h2>
<!-- Content -->
</div>
{/if}<!-- When visible text isn't enough -->
<button aria-label="Close dialog">
<XIcon />
</button>
<!-- For icon-only buttons -->
<button aria-label="Add to favorites">
<HeartIcon />
</button>
<!-- Current state -->
<button aria-pressed={isExpanded}>
Expand section
</button>
<!-- Live regions for updates -->
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div><form onsubmit={handleSubmit}>
<div>
<label for="email">Email Address *</label>
<input
id="email"
type="email"
required
aria-required="true"
aria-invalid={emailError ? 'true' : 'false'}
aria-describedby={emailError ? 'email-error' : undefined}
bind:value={email}
/>
{#if emailError}
<span id="email-error" role="alert" class="error">
{emailError}
</span>
{/if}
</div>
<button type="submit">Submit</button>
</form><!-- 检查对比度比例 -->
<!-- 使用WebAIM对比度检查工具等 -->
<!-- 不佳:浅灰配白色(不达标) -->
<p class="text-gray-400">次要文本</p>
<!-- 良好:深灰色(符合AA标准) -->
<p class="text-gray-600">可读文本</p>
<!-- 使用经过对比度测试的调色板 -->
<p class="text-greens-grove">品牌文本</p><!-- ❌ 无意义的Div嵌套 -->
<div class="card" onclick={handleClick}>
<div class="title">标题</div>
<div class="text">内容</div>
</div>
<!-- ✅ 正确的语义化结构 -->
<article class="card">
<h2>标题</h2>
<p>内容</p>
<button onclick={handleClick}>操作</button>
</article><script>
let modalOpen = $state(false);
let modalRef: HTMLDivElement;
let previousFocus: Element;
function openModal() {
previousFocus = document.activeElement;
modalOpen = true;
// 弹窗打开时聚焦第一个输入框
tick().then(() => {
modalRef?.querySelector('input')?.focus();
});
}
function closeModal() {
modalOpen = false;
// 返回焦点到触发按钮
previousFocus?.focus();
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') closeModal();
}
</script>
{#if modalOpen}
<div
bind:this={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onkeydown={handleKeydown}
>
<h2 id="modal-title">弹窗标题</h2>
<!-- 内容 -->
</div>
{/if}<!-- 当可见文本不足时 -->
<button aria-label="关闭对话框">
<XIcon />
</button>
<!-- 纯图标按钮 -->
<button aria-label="添加到收藏">
<HeartIcon />
</button>
<!-- 当前状态标识 -->
<button aria-pressed={isExpanded}>
展开章节
</button>
<!-- 实时更新区域 -->
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div><form onsubmit={handleSubmit}>
<div>
<label for="email">电子邮箱 *</label>
<input
id="email"
type="email"
required
aria-required="true"
aria-invalid={emailError ? 'true' : 'false'}
aria-describedby={emailError ? 'email-error' : undefined}
bind:value={email}
/>
{#if emailError}
<span id="email-error" role="alert" class="error">
{emailError}
</span>
{/if}
</div>
<button type="submit">提交</button>
</form>// vitest + @axe-core/puppeteer
import { test } from 'vitest';
import { AxePuppeteer } from '@axe-core/puppeteer';
import puppeteer from 'puppeteer';
test('page has no accessibility violations', async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:5173');
const results = await new AxePuppeteer(page).analyze();
expect(results.violations).toEqual([]);
await browser.close();
});npm install --save-dev eslint-plugin-jsx-a11y// vitest + @axe-core/puppeteer
import { test } from 'vitest';
import { AxePuppeteer } from '@axe-core/puppeteer';
import puppeteer from 'puppeteer';
test('页面无无障碍访问违规', async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:5173');
const results = await new AxePuppeteer(page).analyze();
expect(results.violations).toEqual([]);
await browser.close();
});npm install --save-dev eslint-plugin-jsx-a11y
**CI Integration:**
```yaml
**CI集成:**
```yaml
**Documentation:**
```markdown
**文档记录:**
```markdown
**Final Report:**
```markdown
**最终报告:**
```markdown
**Output:** Accessibility maintained with ongoing monitoring
---
**输出:** 通过持续监控维护无障碍访问标准
---| Situation | Action |
|---|---|
| New component | Build with semantic HTML, test keyboard/screen reader |
| Color choice | Check contrast ratio (4.5:1 minimum) |
| Interactive element | Ensure keyboard accessible, visible focus |
| Image | Add descriptive alt text (or alt="" if decorative) |
| Form | Associate labels, error messages, required indicators |
| Animation | Respect prefers-reduced-motion |
| 场景 | 操作 |
|---|---|
| 新组件开发 | 使用语义化HTML,测试键盘/屏幕阅读器交互 |
| 色彩选择 | 检查对比度(最低4.5:1) |
| 可交互元素 | 确保支持键盘访问、焦点可见 |
| 图片 | 添加描述性替代文本(装饰性图片设alt="") |
| 表单 | 关联标签、清晰错误提示、标识必填项 |
| 动画 | 尊重prefers-reduced-motion设置 |