关于 stitch-a11y
用于 Stitch 生成组件的无障碍审查与修复技能,面向准备发布的前端组件。它按 WCAG 2.1 AA 检查语义、键盘操作、焦点、媒体和对比度,并直接修改源文件及生成审查报告。
主要功能
- 01审查六类无障碍问题
- 02修复语义 HTML 与 ARIA
- 03完善键盘导航和焦点管理
- 04检查图片、媒体及颜色对比度
- 05添加跳转链接并生成审查报告
适用场景
- 01发布前检查生成的组件
- 02修复模态框和下拉菜单的键盘操作
- 03审查页面的屏幕阅读器支持
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ gabelul/ stitch-kit --skill stitch-a11y安装后在 Claude Code 中输入/stitch-a11y确认已加载。
附带文件resources/audit-checklist.md
详细介绍
Stitch 无障碍审查与修复
你是一名无障碍工程师。你审查由 Stitch 设计生成的组件,识别违反 WCAG 2.1 AA 的问题,并直接修改源文件。你不只是报告问题,还会修复问题。
请在组件生成后运行此技能。开始审查前,组件应能正常运行。
何时使用此技能
在以下情况下使用此技能:
- 组件已生成且运行正常,需要在发布前进行无障碍审查
- 设计包含复杂的交互模式(模态框、下拉菜单、标签面板、折叠面板、轮播图)
- 用户提到“无障碍”“a11y”“WCAG”“屏幕阅读器”“键盘导航”
- 正在为正式上线或无障碍审查做准备
第 1 步:查找要审查的组件
阅读项目文件结构,找到所有组件文件:
# Next.js / React
find src -name "*.tsx" -not -path "*/node_modules/*"
# SvelteKit
find src -name "*.svelte" -not -path "*/node_modules/*"
审查前先阅读每个组件文件。将精力主要放在交互组件上——与表单、导航、模态框和下拉菜单相比,静态内容需要的关注较少。
第 2 步:审查——6 个类别
针对每个组件,依次系统地检查以下各类别。
类别 1:语义化 HTML
要查找的问题:
- 将
<div>或<span>用于导航、页眉、页脚、主要内容、文章或章节 - 使用
<div onClick>,而不是<button>或<a> - 标题层级顺序错误(例如 h2 前出现 h3,或跳过层级)
- 将表格用于布局(而非数据展示)
- 将列表渲染为普通的
<div>元素
修复方法:
// ❌ Wrong
<div className="nav">
<div onClick={goHome}>Home</div>
</div>
// ✅ Fixed
<nav aria-label="Main navigation">
<a href="/">Home</a>
</nav>
// ❌ Wrong — div button
<div className="btn" onClick={handleClick}>Submit</div>
// ✅ Fixed — real button
<button type="button" onClick={handleClick}>Submit</button>
// ❌ Wrong — visual list as divs
<div className="menu">
<div>Item 1</div>
<div>Item 2</div>
</div>
// ✅ Fixed
<ul role="list">
<li>Item 1</li>
<li>Item 2</li>
</ul>
类别 2:ARIA 属性
只有在语义化 HTML 无法提供足够信息时才添加 ARIA。请记住:没有 ARIA 胜过错误使用 ARIA。
要查找的问题:
- 仅含图标且没有可访问名称的按钮
- 多个
<nav>区域没有aria-label - 存在多个
<main>元素 - 动态更新的状态区域或实时区域没有
aria-live - 交互元素缺少
aria-expanded、aria-haspopup、aria-controls
修复方法:
// Icon-only button
<button aria-label="Close dialog" type="button">
<XIcon aria-hidden="true" />
</button>
// Multiple nav regions
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<nav aria-label="Pagination">...</nav>
// Dropdown toggle
<button
aria-expanded={isOpen}
aria-haspopup="menu"
aria-controls="user-menu"
>
Account
</button>
<ul id="user-menu" role="menu" hidden={!isOpen}>
<li role="menuitem"><a href="/profile">Profile</a></li>
</ul>
// Live status region
<div aria-live="polite" aria-atomic="true" className="sr-only">
{statusMessage}
</div>
类别 3:键盘导航
所有交互元素都必须支持键盘操作。请按以下思路进行检查:用 Tab 键遍历页面——是否能够找到并触发每项操作?
要查找的问题:
- 自定义交互元素无法通过 Tab 键获得焦点
- 在焦点应可到达的元素上使用
tabIndex={-1} - 使用
tabIndex={1}或更大的值(会破坏自然 Tab 顺序) - 打开模态框时,焦点没有移入模态框
- 关闭模态框时,焦点没有返回触发元素
- 按 Escape 关闭下拉菜单时,焦点没有返回触发元素
修复方法:
// Focus management for modal — React
import { useEffect, useRef } from 'react'
export function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (isOpen) {
// Move focus into modal when it opens
modalRef.current?.focus()
}
}, [isOpen])
function handleClose() {
onClose()
// Return focus to trigger when modal closes
triggerRef.current?.focus()
}
return (
<>
<button ref={triggerRef} onClick={() => setIsOpen(true)}>
Open Modal
</button>
{isOpen && (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1} /* Makes div focusable without entering tab order */
>
<h2 id="modal-title">Modal Title</h2>
{children}
<button onClick={handleClose}>Close</button>
</div>
)}
</>
)
}
// Keyboard handler for custom interactive elements
<div
role="button"
tabIndex={0}
onClick={handleAction}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleAction()
}
}}
>
Custom button behavior
</div>
<script lang="ts">
let dialogEl = $state<HTMLDialogElement>()
let triggerEl = $state<HTMLButtonElement>()
let isOpen = $state(false)
function openDialog() {
isOpen = true
// tick() ensures DOM is updated before focusing
tick().then(() => dialogEl?.focus())
}
function closeDialog() {
isOpen = false
triggerEl?.focus() // Return focus to trigger
}
</script>
<button bind:this={triggerEl} onclick={openDialog}>Open</button>
{#if isOpen}
<dialog
bind:this={dialogEl}
tabindex="-1"
aria-modal="true"
onkeydown={(e) => e.key === 'Escape' && closeDialog()}
>
<button onclick={closeDialog}>Close</button>
</dialog>
{/if}
类别 4:焦点可见性
所有交互元素都必须有可见的焦点指示器。移除焦点环时,务必提供同样醒目的替代样式。
要查找的问题:
- 使用
outline: none或outline: 0,但没有自定义焦点样式 - 在 Tailwind 中使用
.focus:outline-none,但没有使用focus-visible:ring-* - 焦点样式只会在点击时显示,而不会在键盘聚焦时显示
修复方法:
在 CSS 中:
/* Never this */
*:focus { outline: none; }
/* Always this — uses :focus-visible to show only on keyboard focus */
*:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 2px;
}
在 Tailwind 中:
// ❌ Wrong
<button className="focus:outline-none">
// ✅ Fixed
<button className="focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2">
类别 5:图像和媒体
要查找的问题:
<img>或<Image>没有alt属性- 有意义的图像使用
alt="" - 装饰性图像使用描述性替代文本(会给屏幕阅读器造成干扰)
- 图标用作交互元素时没有可访问标签
- 视频没有字幕
修复方法:
// Meaningful image
<Image src="/hero.jpg" alt="Team members collaborating at a whiteboard in a modern office" />
// Decorative image — empty alt so screen readers skip it
<Image src="/bg-pattern.svg" alt="" aria-hidden="true" />
// Icon in a button — hide icon, label the button
<button aria-label="Delete item">
<TrashIcon aria-hidden="true" />
</button>
// Icon with adjacent text — hide the icon (it's redundant)
<button>
<SaveIcon aria-hidden="true" />
<span>Save changes</span>
</button>
类别 6:颜色和对比度
不使用自动化工具,通过分析设计检查以下内容:
要查找的问题:
- 柔和文本(
--color-text-muted)放在柔和背景(--color-surface)上——通常无法达到 4.5:1 - 小尺寸文本中的白色背景主色——确认对比度达到 4.5:1
- 禁用状态的文本过浅,即使作为提示也难以阅读
- 仅依靠颜色传达含义(错误状态、必填字段)
修复方法:
// Add non-color indicator for errors
<input
aria-invalid={hasError}
aria-describedby={hasError ? 'email-error' : undefined}
className={hasError ? 'border-error' : 'border-border'}
/>
{hasError && (
<p id="email-error" role="alert" className="text-error">
{/* Icon + text — not color alone */}
<AlertIcon aria-hidden="true" />
Please enter a valid email address
</p>
)}
// Required field indicator
<label>
Email
<span aria-hidden="true" className="text-error"> *</span>
<span className="sr-only">(required)</span>
</label>
第 3 步:sr-only 工具类
如果全局 CSS 中还没有此工具类,请添加。你会经常用到它:
/* Visually hidden, but readable by screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Skip link — visible on focus for keyboard users */
.skip-link {
position: absolute;
left: -9999px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
position: fixed;
top: 1rem;
left: 1rem;
width: auto;
height: auto;
padding: 0.5rem 1rem;
background: var(--color-primary);
color: var(--color-primary-fg);
border-radius: var(--radius-md);
font-weight: 600;
z-index: 9999;
}
第 4 步:添加跳过导航链接
在每个页面布局中,将跳过链接添加为第一个元素。这样,键盘用户就能跳过导航:
// app/layout.tsx or +layout.svelte — first child of <body>
<a href="#main-content" className="skip-link">
Skip to main content
</a>
// The target
<main id="main-content" tabIndex={-1}>
{children}
</main>
第 5 步:生成审计报告
修复问题后,创建 accessibility-audit.md,总结发现和修复的问题:
# Accessibility Audit Report
**Date:** [date]
**WCAG Target:** 2.1 AA
**Components audited:** [list]
## Issues Found & Fixed
### Critical (would block screen reader users)
- [Component]: [issue] → [fix applied]
### Important (keyboard navigation issues)
- [Component]: [issue] → [fix applied]
### Minor (improvements to quality of life)
- [Component]: [issue] → [fix applied]
## Remaining Recommendations
[Any issues that require design changes or user testing to resolve]
## How to test
1. Tab through the entire page — every interactive element should be reachable
2. Activate with Enter/Space — all buttons and links should work
3. Test with VoiceOver (Mac) or NVDA (Windows) — key flows should be narrated correctly
4. Browser DevTools → Rendering → Emulate prefers-reduced-motion → Verify animations stop
5. axe DevTools extension for automated checks
故障排除
| 问题 | 修复方法 |
|---|---|
aria-labelledby 指向错误的 ID | 确保页面中的 ID 唯一 |
| 焦点陷阱将键盘锁定在模态框内 | 在模态框边界内正确实现 Tab/Shift+Tab 循环 |
| 屏幕阅读器播报冗余信息 | 为装饰性元素添加 aria-hidden="true" |
| 同一组件中存在多个违规问题 | 先修复语义化 HTML——ARIA 问题通常由此引发 |
| 跳过链接未显示 | 确保 :focus 状态覆盖屏幕外定位样式 |
参考资料
resources/audit-checklist.md— 发布前审核的快速参考清单