重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
accessibility-implementation by laurigates/claude-plugins
npx skills add https://github.com/laurigates/claude-plugins --skill accessibility-implementationWCAG 指南、ARIA 模式及辅助技术支持的技术实现。
| 标准 | 实现 |
|---|---|
| 1.1.1 非文本内容 | 图像的 alt 属性,输入框的标签 |
| 1.3.1 信息和关系 | 语义化 HTML,ARIA 关系 |
| 2.1.1 键盘 | 所有交互元素均可通过键盘访问 |
| 2.4.1 绕过区块 | 跳过链接、地标 |
| 4.1.2 名称、角色、值 | 自定义控件的 ARIA 标签、角色 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 标准 | 实现 |
|---|
| 1.4.3 对比度 (最低) | 文本 4.5:1,大文本 3:1 |
| 1.4.11 非文本对比度 | UI 组件 3:1 |
| 2.4.6 标题和标签 | 描述性、层次化的标题 |
| 2.4.7 焦点可见 | 可见的焦点指示器 (2px+ 轮廓) |
<!-- 自定义按钮 -->
<div role="button" tabindex="0"
aria-pressed="false"
onkeydown="handleKeyDown(event)">
切换功能
</div>
<!-- 图标按钮 (需要可访问名称) -->
<button aria-label="关闭对话框">
<svg aria-hidden="true">...</svg>
</button>
<!-- 链接 vs 按钮 -->
<!-- 导航用链接,操作用按钮 -->
<a href="/page">前往页面</a>
<button type="button">提交表单</button>
<!-- 带标签的输入框 -->
<label for="email">电子邮件地址</label>
<input id="email" type="email"
aria-describedby="email-hint email-error"
aria-invalid="true"
required>
<div id="email-hint">我们绝不会分享您的电子邮件</div>
<div id="email-error" role="alert">请输入有效的电子邮件</div>
<!-- 复选框组 -->
<fieldset>
<legend>通知偏好设置</legend>
<label><input type="checkbox" name="notif" value="email"> 电子邮件</label>
<label><input type="checkbox" name="notif" value="sms"> 短信</label>
</fieldset>
<!-- 组合框 (自动完成) -->
<label for="country">国家</label>
<input id="country"
role="combobox"
aria-expanded="false"
aria-autocomplete="list"
aria-controls="country-listbox">
<ul id="country-listbox" role="listbox" hidden>
<li role="option" id="opt-us">美国</li>
<li role="option" id="opt-uk">英国</li>
</ul>
<div role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc">
<h2 id="dialog-title">确认操作</h2>
<p id="dialog-desc">您确定要继续吗?</p>
<button>取消</button>
<button>确认</button>
</div>
// 焦点陷阱实现
function trapFocus(dialog: HTMLElement) {
const focusable = dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') {
closeDialog();
}
});
// 将焦点移动到第一个元素
first.focus();
}
<div role="tablist" aria-label="设置选项卡">
<button role="tab"
id="tab-1"
aria-selected="true"
aria-controls="panel-1">
常规
</button>
<button role="tab"
id="tab-2"
aria-selected="false"
aria-controls="panel-2"
tabindex="-1">
隐私
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
常规设置内容
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
隐私设置内容
</div>
// 选项卡键盘导航
tablist.addEventListener('keydown', (e) => {
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const current = tabs.indexOf(document.activeElement as Element);
let next: number;
switch (e.key) {
case 'ArrowRight':
next = (current + 1) % tabs.length;
break;
case 'ArrowLeft':
next = (current - 1 + tabs.length) % tabs.length;
break;
case 'Home':
next = 0;
break;
case 'End':
next = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
(tabs[next] as HTMLElement).focus();
activateTab(tabs[next]);
});
<!-- 状态消息 -->
<div role="status" aria-live="polite">
表单保存成功
</div>
<!-- 警报 (中断) -->
<div role="alert" aria-live="assertive">
错误:连接丢失
</div>
<!-- 进度更新 -->
<div aria-live="polite" aria-atomic="true">
加载中:已完成 45%
</div>
| 按键 | 行为 |
|---|---|
| Tab | 移动到下一个可聚焦元素 |
| Shift+Tab | 移动到上一个可聚焦元素 |
| Enter/Space | 激活按钮,选择选项 |
| Escape | 关闭模态框,取消操作 |
| 方向键 | 在组件内导航 (选项卡、菜单、列表框) |
| Home/End | 跳转到列表的第一项/最后一项 |
// 模态框关闭后返回焦点
const triggerElement = document.activeElement;
openModal();
// 关闭时:
closeModal();
triggerElement?.focus();
// 将焦点移动到错误处
function showValidationErrors() {
const firstError = document.querySelector('[aria-invalid="true"]');
(firstError as HTMLElement)?.focus();
}
// 跳过链接
<a href="#main-content" class="skip-link">跳转到主要内容</a>
<main id="main-content" tabindex="-1">...</main>
// 用于复合控件 (工具栏、菜单、选项卡)
function setRovingTabindex(container: HTMLElement, selector: string) {
const items = container.querySelectorAll(selector);
items.forEach((item, index) => {
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
});
container.addEventListener('keydown', (e) => {
const current = Array.from(items).indexOf(document.activeElement as Element);
let next = current;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
next = (current + 1) % items.length;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
next = (current - 1 + items.length) % items.length;
}
if (next !== current) {
items[current].setAttribute('tabindex', '-1');
items[next].setAttribute('tabindex', '0');
(items[next] as HTMLElement).focus();
e.preventDefault();
}
});
}
# axe-core CLI
npx @axe-core/cli https://localhost:3000
# Lighthouse 无障碍审计
npx lighthouse http://localhost:3000 --only-categories=accessibility --output=json
# pa11y
npx pa11y http://localhost:3000
# jest-axe 用于单元测试
npm install --save-dev jest-axe
// jest-axe 示例
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('component is accessible', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
// Playwright 无障碍测试
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('page should not have accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
键盘导航
屏幕阅读器测试
视觉测试
<!-- 错误:图标按钮无标签 -->
<button><svg>...</svg></button>
<!-- 正确:添加 aria-label -->
<button aria-label="关闭">
<svg aria-hidden="true">...</svg>
</button>
<!-- 错误:占位符作为标签 -->
<input placeholder="电子邮件">
<!-- 正确:正确的标签 -->
<label for="email">电子邮件</label>
<input id="email" type="email">
<!-- 正确:视觉隐藏的标签 -->
<label for="search" class="visually-hidden">搜索</label>
<input id="search" type="search" placeholder="搜索...">
<!-- 错误:跳过标题层级 -->
<h1>页面标题</h1>
<h3>章节</h3> <!-- 缺少 h2 -->
<!-- 正确:正确的层级 -->
<h1>页面标题</h1>
<h2>章节</h2>
<h3>子章节</h3>
/* 错误:移除焦点轮廓 */
button:focus { outline: none; }
/* 正确:自定义焦点指示器 */
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* 错误:低对比度 */
.text { color: #999; background: #fff; } /* 2.85:1 比例 */
/* 正确:足够的对比度 */
.text { color: #595959; background: #fff; } /* 4.56:1 比例 */
/* 视觉隐藏但可访问 */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* 跳过链接 */
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
/* 减少动画 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
在使用 ARIA 之前,优先使用原生 HTML 元素。<button> 优于 <div role="button">。
原生元素具有内置的无障碍功能。不要用 JavaScript 破坏它。
自动化工具只能发现约 30% 的问题。使用辅助技术进行手动测试至关重要。
为所有交互提供键盘、鼠标和触摸替代方案。
每周安装次数
54
代码仓库
GitHub 星标数
23
首次出现
2026年1月29日
安全审计
安装于
opencode53
github-copilot53
gemini-cli52
codex52
kimi-cli52
amp52
Technical implementation of WCAG guidelines, ARIA patterns, and assistive technology support.
| Criterion | Implementation |
|---|---|
| 1.1.1 Non-text Content | alt for images, labels for inputs |
| 1.3.1 Info and Relationships | Semantic HTML, ARIA relationships |
| 2.1.1 Keyboard | All interactive elements keyboard accessible |
| 2.4.1 Bypass Blocks | Skip links, landmarks |
| 4.1.2 Name, Role, Value | ARIA labels, roles for custom widgets |
| Criterion | Implementation |
|---|---|
| 1.4.3 Contrast (Minimum) | 4.5:1 text, 3:1 large text |
| 1.4.11 Non-text Contrast | 3:1 for UI components |
| 2.4.6 Headings and Labels | Descriptive, hierarchical headings |
| 2.4.7 Focus Visible | Visible focus indicator (2px+ outline) |
<!-- Custom button -->
<div role="button" tabindex="0"
aria-pressed="false"
onkeydown="handleKeyDown(event)">
Toggle Feature
</div>
<!-- Icon button (needs accessible name) -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<!-- Link vs button -->
<!-- Use link for navigation, button for actions -->
<a href="/page">Go to page</a>
<button type="button">Submit form</button>
<!-- Input with label -->
<label for="email">Email address</label>
<input id="email" type="email"
aria-describedby="email-hint email-error"
aria-invalid="true"
required>
<div id="email-hint">We'll never share your email</div>
<div id="email-error" role="alert">Please enter a valid email</div>
<!-- Checkbox group -->
<fieldset>
<legend>Notification preferences</legend>
<label><input type="checkbox" name="notif" value="email"> Email</label>
<label><input type="checkbox" name="notif" value="sms"> SMS</label>
</fieldset>
<!-- Combobox (autocomplete) -->
<label for="country">Country</label>
<input id="country"
role="combobox"
aria-expanded="false"
aria-autocomplete="list"
aria-controls="country-listbox">
<ul id="country-listbox" role="listbox" hidden>
<li role="option" id="opt-us">United States</li>
<li role="option" id="opt-uk">United Kingdom</li>
</ul>
<div role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc">
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-desc">Are you sure you want to proceed?</p>
<button>Cancel</button>
<button>Confirm</button>
</div>
// Focus trap implementation
function trapFocus(dialog: HTMLElement) {
const focusable = dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') {
closeDialog();
}
});
// Move focus to first element
first.focus();
}
<div role="tablist" aria-label="Settings tabs">
<button role="tab"
id="tab-1"
aria-selected="true"
aria-controls="panel-1">
General
</button>
<button role="tab"
id="tab-2"
aria-selected="false"
aria-controls="panel-2"
tabindex="-1">
Privacy
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
General settings content
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
Privacy settings content
</div>
// Tab keyboard navigation
tablist.addEventListener('keydown', (e) => {
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const current = tabs.indexOf(document.activeElement as Element);
let next: number;
switch (e.key) {
case 'ArrowRight':
next = (current + 1) % tabs.length;
break;
case 'ArrowLeft':
next = (current - 1 + tabs.length) % tabs.length;
break;
case 'Home':
next = 0;
break;
case 'End':
next = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
(tabs[next] as HTMLElement).focus();
activateTab(tabs[next]);
});
<!-- Status messages -->
<div role="status" aria-live="polite">
Form saved successfully
</div>
<!-- Alerts (interrupts) -->
<div role="alert" aria-live="assertive">
Error: Connection lost
</div>
<!-- Progress updates -->
<div aria-live="polite" aria-atomic="true">
Loading: 45% complete
</div>
| Key | Behavior |
|---|---|
| Tab | Move to next focusable element |
| Shift+Tab | Move to previous focusable element |
| Enter/Space | Activate button, select option |
| Escape | Close modal, cancel operation |
| Arrow keys | Navigate within component (tabs, menu, listbox) |
| Home/End | Go to first/last item in list |
// Return focus after modal close
const triggerElement = document.activeElement;
openModal();
// On close:
closeModal();
triggerElement?.focus();
// Move focus to error
function showValidationErrors() {
const firstError = document.querySelector('[aria-invalid="true"]');
(firstError as HTMLElement)?.focus();
}
// Skip link
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content" tabindex="-1">...</main>
// For composite widgets (toolbar, menu, tabs)
function setRovingTabindex(container: HTMLElement, selector: string) {
const items = container.querySelectorAll(selector);
items.forEach((item, index) => {
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
});
container.addEventListener('keydown', (e) => {
const current = Array.from(items).indexOf(document.activeElement as Element);
let next = current;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
next = (current + 1) % items.length;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
next = (current - 1 + items.length) % items.length;
}
if (next !== current) {
items[current].setAttribute('tabindex', '-1');
items[next].setAttribute('tabindex', '0');
(items[next] as HTMLElement).focus();
e.preventDefault();
}
});
}
# axe-core CLI
npx @axe-core/cli https://localhost:3000
# Lighthouse accessibility audit
npx lighthouse http://localhost:3000 --only-categories=accessibility --output=json
# pa11y
npx pa11y http://localhost:3000
# jest-axe for unit tests
npm install --save-dev jest-axe
// jest-axe example
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('component is accessible', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
// Playwright accessibility testing
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('page should not have accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
Keyboard Navigation
Screen Reader Testing
Visual Testing
<!-- Bad: Icon button without label -->
<button><svg>...</svg></button>
<!-- Good: Add aria-label -->
<button aria-label="Close">
<svg aria-hidden="true">...</svg>
</button>
<!-- Bad: Placeholder as label -->
<input placeholder="Email">
<!-- Good: Proper label -->
<label for="email">Email</label>
<input id="email" type="email">
<!-- Good: Visually hidden label -->
<label for="search" class="visually-hidden">Search</label>
<input id="search" type="search" placeholder="Search...">
<!-- Bad: Skipping heading levels -->
<h1>Page Title</h1>
<h3>Section</h3> <!-- Missing h2 -->
<!-- Good: Proper hierarchy -->
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
/* Bad: Removing focus outline */
button:focus { outline: none; }
/* Good: Custom focus indicator */
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Bad: Low contrast */
.text { color: #999; background: #fff; } /* 2.85:1 ratio */
/* Good: Sufficient contrast */
.text { color: #595959; background: #fff; } /* 4.56:1 ratio */
/* Visually hidden but accessible */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Skip link */
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Use native HTML elements before ARIA. A <button> is better than <div role="button">.
Native elements have built-in accessibility. Don't break it with JavaScript.
Automated tools catch ~30% of issues. Manual testing with assistive technology is essential.
Offer keyboard, mouse, and touch alternatives for all interactions.
Weekly Installs
54
Repository
GitHub Stars
23
First Seen
Jan 29, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode53
github-copilot53
gemini-cli52
codex52
kimi-cli52
amp52
React视图过渡API使用指南:实现原生浏览器动画与状态管理
7,500 周安装
Swift 6+ 最佳实践指南:iOS/macOS 开发、并发安全与代码规范
121 周安装
LangChain开发指南:Python构建LLM应用最佳实践与架构设计
121 周安装
canghe-url-to-markdown:使用Chrome CDP将网页URL转换为干净Markdown的工具
120 周安装
OCR图像文字识别技能 - 支持100+语言,Python调用Tesseract提取图片文本
54 周安装
Capgo Live Updates:Capacitor应用实时OTA更新,无需应用商店审核
121 周安装
review-and-ship技能:AI助手代码审查与部署工具,提升开发效率
124 周安装