mcpField
S
stitch-a11y

stitch-a11y

github.com/gabelul测试与质量4740 次安装14 周前更新
代码仓库

关于 stitch-a11y

用于 Stitch 生成组件的无障碍审查与修复技能,面向准备发布的前端组件。它按 WCAG 2.1 AA 检查语义、键盘操作、焦点、媒体和对比度,并直接修改源文件及生成审查报告。

主要功能

  1. 01审查六类无障碍问题
  2. 02修复语义 HTML 与 ARIA
  3. 03完善键盘导航和焦点管理
  4. 04检查图片、媒体及颜色对比度
  5. 05添加跳转链接并生成审查报告

适用场景

  1. 01发布前检查生成的组件
  2. 02修复模态框和下拉菜单的键盘操作
  3. 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 — 发布前审核的快速参考清单

评论

支持 Markdown