重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
accessibility by tech-leads-club/agent-skills
npx skills add https://github.com/tech-leads-club/agent-skills --skill accessibility基于 WCAG 2.1 和 Lighthouse 无障碍性审计的全面无障碍性指南。目标:使内容可供所有人使用,包括残障人士。
| 原则 | 描述 |
|---|---|
| P 可感知 | 内容可以通过不同的感官感知 |
| O 可操作 | 界面可供所有用户操作 |
| U 可理解 | 内容和界面易于理解 |
| R 健壮 | 内容可与辅助技术配合使用 |
| 级别 | 要求 | 目标 |
|---|---|---|
| A | 最低无障碍性 | 必须通过 |
| AA | 标准合规性 | 应该通过(在许多司法管辖区是法律要求) |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| AAA | 增强的无障碍性 | 最好具备 |
图像需要替代文本:
<!-- ❌ 缺少 alt -->
<img src="chart.png" />
<!-- ✅ 描述性 alt -->
<img src="chart.png" alt="显示第三季度销售额增长 40% 的条形图" />
<!-- ✅ 装饰性图像(空 alt) -->
<img src="decorative-border.png" alt="" role="presentation" />
<!-- ✅ 带有较长描述的复杂图像 -->
<figure>
<img src="infographic.png" alt="2024 市场趋势信息图" aria-describedby="infographic-desc" />
<figcaption id="infographic-desc">
<!-- 详细描述 -->
</figcaption>
</figure>
图标按钮需要可访问的名称:
<!-- ❌ 没有可访问的名称 -->
<button>
<svg><!-- 菜单图标 --></svg>
</button>
<!-- ✅ 使用 aria-label -->
<button aria-label="打开菜单">
<svg aria-hidden="true"><!-- 菜单图标 --></svg>
</button>
<!-- ✅ 使用视觉隐藏文本 -->
<button>
<svg aria-hidden="true"><!-- 菜单图标 --></svg>
<span class="visually-hidden">打开菜单</span>
</button>
视觉隐藏类:
.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;
}
| 文本大小 | AA 最低要求 | AAA 增强要求 |
|---|---|---|
| 普通文本(< 18px / < 14px 粗体) | 4.5:1 | 7:1 |
| 大文本(≥ 18px / ≥ 14px 粗体) | 3:1 | 4.5:1 |
| UI 组件和图形 | 3:1 | 3:1 |
/* ❌ 低对比度 (2.5:1) */
.low-contrast {
color: #999;
background: #fff;
}
/* ✅ 足够的对比度 (7:1) */
.high-contrast {
color: #333;
background: #fff;
}
/* ✅ 焦点状态也需要对比度 */
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
不要仅依赖颜色:
<!-- ❌ 仅用颜色表示错误 -->
<input class="error-border" />
<style>
.error-border {
border-color: red;
}
</style>
<!-- ✅ 颜色 + 图标 + 文本 -->
<div class="field-error">
<input aria-invalid="true" aria-describedby="email-error" />
<span id="email-error" class="error-message">
<svg aria-hidden="true"><!-- 错误图标 --></svg>
请输入有效的电子邮件地址
</span>
</div>
<!-- 带字幕的视频 -->
<video controls>
<source src="video.mp4" type="video/mp4" />
<track kind="captions" src="captions.vtt" srclang="en" label="English" default />
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions" />
</video>
<!-- 带文字稿的音频 -->
<audio controls>
<source src="podcast.mp3" type="audio/mp3" />
</audio>
<details>
<summary>文字稿</summary>
<p>完整的文字稿文本...</p>
</details>
所有功能必须可通过键盘访问:
// ❌ 仅处理点击
element.addEventListener('click', handleAction)
// ✅ 处理点击和键盘
element.addEventListener('click', handleAction)
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleAction()
}
})
无键盘陷阱:
// 模态框焦点管理
function openModal(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
)
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
// 将焦点限制在模态框内
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault()
lastElement.focus()
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault()
firstElement.focus()
}
}
if (e.key === 'Escape') {
closeModal()
}
})
firstElement.focus()
}
/* ❌ 永远不要移除焦点轮廓 */
*:focus {
outline: none;
}
/* ✅ 对仅键盘焦点使用 :focus-visible */
:focus {
outline: none;
}
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* ✅ 或自定义焦点样式 */
button:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
}
<body>
<a href="#main-content" class="skip-link">跳转到主要内容</a>
<header><!-- 导航 --></header>
<main id="main-content" tabindex="-1">
<!-- 主要内容 -->
</main>
</body>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
// 允许用户延长时间限制
function showSessionWarning() {
const modal = createModal({
title: '会话即将过期',
content: '您的会话将在 2 分钟后过期。',
actions: [
{ label: '延长会话', action: extendSession },
{ label: '退出登录', action: logout },
],
timeout: 120000, // 2 分钟响应时间
})
}
/* 尊重减少动效偏好 */
@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;
}
}
<!-- ❌ 未指定语言 -->
<html>
<!-- ✅ 指定语言 -->
<html lang="en">
<!-- ✅ 页面内语言变更 -->
<p>法语中 hello 是 <span lang="fr">bonjour</span>。</p>
</html>
</html>
<!-- 导航应在各页面间保持一致 -->
<nav aria-label="Main">
<ul>
<li><a href="/" aria-current="page">首页</a></li>
<li><a href="/products">产品</a></li>
<li><a href="/about">关于</a></li>
</ul>
</nav>
<!-- ❌ 没有标签关联 -->
<input type="email" placeholder="Email" />
<!-- ✅ 显式标签 -->
<label for="email">电子邮件地址</label>
<input type="email" id="email" name="email" autocomplete="email" required />
<!-- ✅ 隐式标签 -->
<label>
电子邮件地址
<input type="email" name="email" autocomplete="email" required />
</label>
<!-- ✅ 带有说明 -->
<label for="password">密码</label>
<input type="password" id="password" aria-describedby="password-requirements" />
<p id="password-requirements">必须至少 8 个字符,包含一个数字。</p>
<!-- 向屏幕阅读器宣布错误 -->
<form novalidate>
<div class="field" aria-live="polite">
<label for="email">电子邮件</label>
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error" />
<p id="email-error" class="error" role="alert">请输入有效的电子邮件地址(例如,name@example.com)</p>
</div>
</form>
// 提交时聚焦第一个错误
form.addEventListener('submit', (e) => {
const firstError = form.querySelector('[aria-invalid="true"]')
if (firstError) {
e.preventDefault()
firstError.focus()
// 宣布错误摘要
const errorSummary = document.getElementById('error-summary')
errorSummary.textContent = `发现 ${errors.length} 个错误。请修正后重试。`
errorSummary.focus()
}
})
<!-- ❌ 重复的 ID -->
<div id="content">...</div>
<div id="content">...</div>
<!-- ❌ 无效的嵌套 -->
<a href="/"><button>点击</button></a>
<!-- ✅ 唯一的 ID -->
<div id="main-content">...</div>
<div id="sidebar-content">...</div>
<!-- ✅ 正确的嵌套 -->
<a href="/" class="button-link">点击</a>
优先使用原生元素:
<!-- ❌ 在 div 上使用 ARIA 角色 -->
<div role="button" tabindex="0">点击我</div>
<!-- ✅ 原生按钮 -->
<button>点击我</button>
<!-- ❌ ARIA 复选框 -->
<div role="checkbox" aria-checked="false">选项</div>
<!-- ✅ 原生复选框 -->
<label><input type="checkbox" /> 选项</label>
当需要 ARIA 时:
<!-- 自定义标签页组件 -->
<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>
<!-- 状态更新 -->
<div aria-live="polite" aria-atomic="true" class="status">
<!-- 内容更新会向屏幕阅读器宣布 -->
</div>
<!-- 紧急警报 -->
<div role="alert" aria-live="assertive">
<!-- 中断当前宣布 -->
</div>
// 宣布动态内容变化
function showNotification(message, type = 'polite') {
const container = document.getElementById(`${type}-announcer`)
container.textContent = '' // 先清空
requestAnimationFrame(() => {
container.textContent = message
})
}
# Lighthouse 无障碍性审计
npx lighthouse https://example.com --only-categories=accessibility
# axe-core
npm install @axe-core/cli -g
axe https://example.com
prefers-reduced-motion: reduce 测试| 操作 | VoiceOver(Mac) | NVDA(Windows) |
|---|---|---|
| 开始/停止 | ⌘ + F5 | Ctrl + Alt + N |
| 下一个项目 | VO + → | ↓ |
| 上一个项目 | VO + ← | ↑ |
| 激活 | VO + Space | Enter |
| 标题列表 | VO + U,然后箭头键 | H / Shift + H |
| 链接列表 | VO + U | K / Shift + K |
每周安装量
60
仓库
GitHub 星标数
1.9K
首次出现
2026 年 2 月 5 日
安全审计
安装于
opencode58
gemini-cli56
codex56
github-copilot56
cursor55
amp53
Comprehensive accessibility guidelines based on WCAG 2.1 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities.
| Principle | Description |
|---|---|
| P erceivable | Content can be perceived through different senses |
| O perable | Interface can be operated by all users |
| U nderstandable | Content and interface are understandable |
| R obust | Content works with assistive technologies |
| Level | Requirement | Target |
|---|---|---|
| A | Minimum accessibility | Must pass |
| AA | Standard compliance | Should pass (legal requirement in many jurisdictions) |
| AAA | Enhanced accessibility | Nice to have |
Images require alt text:
<!-- ❌ Missing alt -->
<img src="chart.png" />
<!-- ✅ Descriptive alt -->
<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales" />
<!-- ✅ Decorative image (empty alt) -->
<img src="decorative-border.png" alt="" role="presentation" />
<!-- ✅ Complex image with longer description -->
<figure>
<img src="infographic.png" alt="2024 market trends infographic" aria-describedby="infographic-desc" />
<figcaption id="infographic-desc">
<!-- Detailed description -->
</figcaption>
</figure>
Icon buttons need accessible names:
<!-- ❌ No accessible name -->
<button>
<svg><!-- menu icon --></svg>
</button>
<!-- ✅ Using aria-label -->
<button aria-label="Open menu">
<svg aria-hidden="true"><!-- menu icon --></svg>
</button>
<!-- ✅ Using visually hidden text -->
<button>
<svg aria-hidden="true"><!-- menu icon --></svg>
<span class="visually-hidden">Open menu</span>
</button>
Visually hidden class:
.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;
}
| Text Size | AA minimum | AAA enhanced |
|---|---|---|
| Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 |
| Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 |
| UI components & graphics | 3:1 | 3:1 |
/* ❌ Low contrast (2.5:1) */
.low-contrast {
color: #999;
background: #fff;
}
/* ✅ Sufficient contrast (7:1) */
.high-contrast {
color: #333;
background: #fff;
}
/* ✅ Focus states need contrast too */
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
Don't rely on color alone:
<!-- ❌ Only color indicates error -->
<input class="error-border" />
<style>
.error-border {
border-color: red;
}
</style>
<!-- ✅ Color + icon + text -->
<div class="field-error">
<input aria-invalid="true" aria-describedby="email-error" />
<span id="email-error" class="error-message">
<svg aria-hidden="true"><!-- error icon --></svg>
Please enter a valid email address
</span>
</div>
<!-- Video with captions -->
<video controls>
<source src="video.mp4" type="video/mp4" />
<track kind="captions" src="captions.vtt" srclang="en" label="English" default />
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions" />
</video>
<!-- Audio with transcript -->
<audio controls>
<source src="podcast.mp3" type="audio/mp3" />
</audio>
<details>
<summary>Transcript</summary>
<p>Full transcript text...</p>
</details>
All functionality must be keyboard accessible:
// ❌ Only handles click
element.addEventListener('click', handleAction)
// ✅ Handles both click and keyboard
element.addEventListener('click', handleAction)
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleAction()
}
})
No keyboard traps:
// Modal focus management
function openModal(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
)
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
// Trap focus within modal
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault()
lastElement.focus()
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault()
firstElement.focus()
}
}
if (e.key === 'Escape') {
closeModal()
}
})
firstElement.focus()
}
/* ❌ Never remove focus outlines */
*:focus {
outline: none;
}
/* ✅ Use :focus-visible for keyboard-only focus */
:focus {
outline: none;
}
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* ✅ Or custom focus styles */
button:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
}
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- navigation --></header>
<main id="main-content" tabindex="-1">
<!-- main content -->
</main>
</body>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
// Allow users to extend time limits
function showSessionWarning() {
const modal = createModal({
title: 'Session Expiring',
content: 'Your session will expire in 2 minutes.',
actions: [
{ label: 'Extend session', action: extendSession },
{ label: 'Log out', action: logout },
],
timeout: 120000, // 2 minutes to respond
})
}
/* Respect reduced motion preference */
@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;
}
}
<!-- ❌ No language specified -->
<html>
<!-- ✅ Language specified -->
<html lang="en">
<!-- ✅ Language changes within page -->
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>
</html>
</html>
<!-- Navigation should be consistent across pages -->
<nav aria-label="Main">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/products">Products</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<!-- ❌ No label association -->
<input type="email" placeholder="Email" />
<!-- ✅ Explicit label -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" autocomplete="email" required />
<!-- ✅ Implicit label -->
<label>
Email address
<input type="email" name="email" autocomplete="email" required />
</label>
<!-- ✅ With instructions -->
<label for="password">Password</label>
<input type="password" id="password" aria-describedby="password-requirements" />
<p id="password-requirements">Must be at least 8 characters with one number.</p>
<!-- Announce errors to screen readers -->
<form novalidate>
<div class="field" aria-live="polite">
<label for="email">Email</label>
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error" />
<p id="email-error" class="error" role="alert">Please enter a valid email address (e.g., name@example.com)</p>
</div>
</form>
// Focus first error on submit
form.addEventListener('submit', (e) => {
const firstError = form.querySelector('[aria-invalid="true"]')
if (firstError) {
e.preventDefault()
firstError.focus()
// Announce error summary
const errorSummary = document.getElementById('error-summary')
errorSummary.textContent = `${errors.length} errors found. Please fix them and try again.`
errorSummary.focus()
}
})
<!-- ❌ Duplicate IDs -->
<div id="content">...</div>
<div id="content">...</div>
<!-- ❌ Invalid nesting -->
<a href="/"><button>Click</button></a>
<!-- ✅ Unique IDs -->
<div id="main-content">...</div>
<div id="sidebar-content">...</div>
<!-- ✅ Proper nesting -->
<a href="/" class="button-link">Click</a>
Prefer native elements:
<!-- ❌ ARIA role on div -->
<div role="button" tabindex="0">Click me</div>
<!-- ✅ Native button -->
<button>Click me</button>
<!-- ❌ ARIA checkbox -->
<div role="checkbox" aria-checked="false">Option</div>
<!-- ✅ Native checkbox -->
<label><input type="checkbox" /> Option</label>
When ARIA is needed:
<!-- Custom tabs component -->
<div role="tablist" aria-label="Product information">
<button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">Description</button>
<button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">Reviews</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
<!-- Panel content -->
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
<!-- Panel content -->
</div>
<!-- Status updates -->
<div aria-live="polite" aria-atomic="true" class="status">
<!-- Content updates announced to screen readers -->
</div>
<!-- Urgent alerts -->
<div role="alert" aria-live="assertive">
<!-- Interrupts current announcement -->
</div>
// Announce dynamic content changes
function showNotification(message, type = 'polite') {
const container = document.getElementById(`${type}-announcer`)
container.textContent = '' // Clear first
requestAnimationFrame(() => {
container.textContent = message
})
}
# Lighthouse accessibility audit
npx lighthouse https://example.com --only-categories=accessibility
# axe-core
npm install @axe-core/cli -g
axe https://example.com
prefers-reduced-motion: reduce| Action | VoiceOver (Mac) | NVDA (Windows) |
|---|---|---|
| Start/Stop | ⌘ + F5 | Ctrl + Alt + N |
| Next item | VO + → | ↓ |
| Previous item | VO + ← | ↑ |
| Activate | VO + Space | Enter |
| Headings list | VO + U, then arrows | H / Shift + H |
| Links list | VO + U | K / Shift + K |
Weekly Installs
60
Repository
GitHub Stars
1.9K
First Seen
Feb 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode58
gemini-cli56
codex56
github-copilot56
cursor55
amp53
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
123,700 周安装
deepTools:NGS数据分析工具包 - ChIP-seq/RNA-seq/ATAC-seq质量控制与可视化
55 周安装
scikit-bio Python生物信息学库:序列分析、系统发育、多样性计算与微生物组数据处理
55 周安装
Python DICOM处理教程:pydicom读取医学影像、元数据操作与格式转换
55 周安装
治疗计划撰写模板与工具 - 专业LaTeX模板、AI图表生成、法规合规
55 周安装
Geniml:基因组区间机器学习Python包,支持BED文件嵌入与单细胞ATAC-seq分析
55 周安装
PennyLane量子计算库:量子机器学习、量子化学与硬件集成全指南
55 周安装