mcpField
A
accessibility

accessibility

github.com/jezweb设计与 UI1.1k505 次安装3 周前更新
代码仓库

关于 accessibility

accessibility 是面向网页设计与开发者的无障碍实践技能,围绕 WCAG 2.1 AA 提供跨框架指南。内容涵盖语义 HTML、键盘操作、对比度、表单和测试流程。

主要功能

  1. 01语义 HTML 与 ARIA 使用指南
  2. 02键盘导航和焦点管理
  3. 03WCAG AA 色彩对比要求
  4. 04可访问表单与错误提示
  5. 05键盘、屏幕阅读器及自动化测试

适用场景

  1. 01检查网页是否符合 WCAG 2.1 AA
  2. 02改进对话框、标签页和表单的可访问性
  3. 03为键盘与屏幕阅读器用户调整交互
  4. 04通过自动化工具检查无障碍问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill accessibility

安装后在 Claude Code 中输入/accessibility确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
agents/
a11y-auditor.md
references/
aria-patterns.md
color-contrast.md
focus-management.md
forms-validation.md
semantic-html.md
wcag-checklist.md
rules/
accessibility.md

详细介绍

Web Accessibility (WCAG 2.1 AA)

状态:已可投入生产 ✅ 最后更新:2026-01-14 依赖项:无(与框架无关) 标准:WCAG 2.1 AA 级


快速入门(5 分钟)

1. 语义化 HTML 基础

选择合适的元素——不要所有内容都用 div:


<div onclick="submit()">Submit</div>
<div onclick="navigate()">Next page</div>


<button type="submit">Submit</button>
<a href="/next">Next page</a>

为什么这很重要:

  • 语义化元素内置键盘支持
  • 屏幕阅读器会自动播报角色
  • 浏览器会提供默认的无障碍行为
2. 焦点管理

确保交互元素可通过键盘访问:

/* ❌ WRONG - removes focus outline */
button:focus { outline: none; }

/* ✅ CORRECT - custom accessible outline */
button:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

重要:

  • 移除焦点轮廓时,绝不能不提供替代方案
  • 使用 :focus-visible,仅在键盘聚焦时显示
  • 确保焦点指示器的对比度达到 3:1
3. 文本替代内容

每个非文本元素都需要文本替代内容:


<img src="logo.png">
<button><svg>...</svg></button>


<img src="logo.png" alt="Company Name">
<button aria-label="Close dialog"><svg>...</svg></button>

无障碍访问的 5 个步骤

步骤 1:选择语义化 HTML

元素选择决策树:

Need clickable element?
├─ Navigates to another page? → <a href="...">
├─ Submits form? → <button type="submit">
├─ Opens dialog? → <button aria-haspopup="dialog">
└─ Other action? → <button type="button">

Grouping content?
├─ Self-contained article? → <article>
├─ Thematic section? → <section>
├─ Navigation links? → <nav>
└─ Supplementary info? → <aside>

Form element?
├─ Text input? → <input type="text">
├─ Multiple choice? → <select> or <input type="radio">
├─ Toggle? → <input type="checkbox"> or <button aria-pressed>
└─ Long text? → <textarea>

完整指南请参见 references/semantic-html.md。

步骤 2:按需添加 ARIA

黄金法则:只有在 HTML 无法表达相应模式时才使用 ARIA。


<button role="button">Click me</button>  


<div role="dialog" aria-labelledby="title" aria-modal="true">
  <h2 id="title">Confirm action</h2>
  
</div>


<dialog aria-labelledby="title">
  <h2 id="title">Confirm action</h2>
</dialog>

常见的 ARIA 模式:

  • aria-label - 不存在可见标签时使用
  • aria-labelledby - 将现有文本作为标签引用
  • aria-describedby - 提供补充说明
  • aria-live - 播报动态更新
  • aria-expanded - 表示可折叠/展开状态

完整模式请参见 references/aria-patterns.md。

步骤 3:实现键盘导航

所有交互元素都必须可通过键盘访问:

// Tab order management
function Dialog({ onClose }) {
  const dialogRef = useRef<HTMLDivElement>(null);
  const previousFocus = useRef<HTMLElement | null>(null);

  useEffect(() => {
    // Save previous focus
    previousFocus.current = document.activeElement as HTMLElement;

    // Focus first element in dialog
    const firstFocusable = dialogRef.current?.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
    (firstFocusable as HTMLElement)?.focus();

    // Trap focus within dialog
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'Tab') {
        // Focus trap logic here
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      // Restore focus on close
      previousFocus.current?.focus();
    };
  }, [onClose]);

  return <div ref={dialogRef} role="dialog">...</div>;
}

基本键盘操作模式:

  • Tab/Shift+Tab:在可聚焦元素之间导航
  • Enter/Space:激活按钮/链接
  • 方向键:在组件内导航(标签页、菜单)
  • Escape:关闭对话框/菜单
  • Home/End:跳转到第一个/最后一个项目

完整模式请参见 references/focus-management.md。

步骤 4:确保颜色对比度

WCAG AA 要求:

  • 普通文本(小于 18pt):对比度为 4.5:1
  • 大号文本(18pt 及以上,或 14pt 及以上的粗体文本):对比度为 3:1
  • UI 组件(按钮、边框):对比度为 3:1
/* ❌ WRONG - insufficient contrast */
:root {
  --background: #ffffff;
  --text: #999999;  /* 2.8:1 - fails WCAG AA */
}

/* ✅ CORRECT - sufficient contrast */
:root {
  --background: #ffffff;
  --text: #595959;  /* 4.6:1 - passes WCAG AA */
}

测试工具:

  • 浏览器开发者工具(Chrome/Firefox 内置检查工具)
  • 对比度检查器扩展
  • axe DevTools 扩展

完整指南请参见 references/color-contrast.md。

步骤 5:确保表单无障碍

每个表单输入项都需要可见标签:


<input type="email" placeholder="Email address">


<label for="email">Email address</label>
<input type="email" id="email" name="email" required aria-required="true">

错误处理:

<label for="email">Email address</label>
<input
  type="email"
  id="email"
  name="email"
  aria-invalid="true"
  aria-describedby="email-error"
>
<span id="email-error" role="alert">
  Please enter a valid email address
</span>

用于动态错误的实时区域:

<div role="alert" aria-live="assertive" aria-atomic="true">
  Form submission failed. Please fix the errors above.
</div>

完整模式请参见 references/forms-validation.md。


关键规则

始终做到

✅ 优先使用语义化 HTML 元素(button、a、nav、article 等) ✅ 为所有非文本内容提供文本替代内容 ✅ 确保普通文本的对比度为 4.5:1,大号文本/UI 的对比度为 3:1 ✅ 确保所有功能都可通过键盘访问 ✅ 仅使用键盘进行测试(拔掉鼠标) ✅ 使用屏幕阅读器进行测试(Windows 上的 NVDA、Mac 上的 VoiceOver) ✅ 使用正确的标题层级(h1 → h2 → h3,不跳级) ✅ 为所有表单输入项添加可见标签 ✅ 提供焦点指示器(绝不能只使用 outline: none) ✅ 使用 aria-live 更新动态内容

绝不这样做

❌ 使用带 onClick 的 div 替代 button ❌ 移除焦点轮廓而不提供替代方案 ❌ 仅通过颜色传达信息 ❌ 使用占位符作为标签 ❌ 跳过标题级别(h1 → h3) ❌ 使用大于 0 的 tabindex(会打乱自然顺序) ❌ 存在语义化 HTML 时仍添加 ARIA ❌ 关闭对话框后忘记恢复焦点 ❌ 在可聚焦元素上使用 role="presentation" ❌ 创建键盘陷阱(无法退出)


预防已知问题

此技能可预防 12 个已记录的无障碍问题:

问题 #1:缺少焦点指示器

错误:交互元素没有可见的焦点指示器 来源:WCAG 2.4.7(焦点可见) 发生原因:CSS 重置移除了默认轮廓 预防方法:始终提供自定义的 focus-visible 样式

问题 #2:颜色对比度不足

错误:文本的对比度低于 4.5:1 来源:WCAG 1.4.3(对比度最低标准) 原因:在白色背景上使用浅灰色文本 预防措施:使用对比度检查工具测试所有文本颜色

问题 #3:缺少替代文本

错误:图像缺少 alt 属性 来源:WCAG 1.1.1(非文本内容) 原因:忘记添加,或以为这项要求是可选的 预防措施:装饰性图像添加 alt="",有意义的图像添加描述性 alt

问题 #4:键盘导航失效

错误:无法通过键盘访问交互元素 来源:WCAG 2.1.1(键盘) 原因:使用带有 onClick 的 div,而不是 button 预防措施:使用语义化交互元素(button、a)

问题 #5:表单输入框没有标签

错误:输入框缺少关联标签 来源:WCAG 3.3.2(标签或说明) 原因:将占位符用作标签 预防措施:始终使用 <label> 元素,并通过 for/id 建立关联

问题 #6:标题级别跳跃

错误:标题层级从 h1 跳到 h3 来源:WCAG 1.3.1(信息与关系) 原因:使用标题进行视觉样式设计,而非表达语义 预防措施:按顺序使用标题,并通过 CSS 设置样式

问题 #7:对话框中没有焦点陷阱

错误:按 Tab 键会从对话框跳转到背景内容 来源:WCAG 2.4.3(焦点顺序) 原因:未实现焦点陷阱 预防措施:为模态对话框实现焦点陷阱

问题 #8:动态内容缺少 aria-live

错误:屏幕阅读器不会播报更新 来源:WCAG 4.1.3(状态消息) 原因:添加动态内容时未进行播报 预防措施:使用 aria-live="polite" 或 "assertive"

问题 #9:仅用颜色传达信息

错误:仅使用颜色传达状态 来源:WCAG 1.4.1(颜色的使用) 原因:用红色文本标记错误,却没有图标或文本说明 预防措施:添加图标和文本标签,而不只是使用颜色

问题 #10:链接文本描述不清

错误:链接文本使用“点击此处”或“阅读更多” 来源:WCAG 2.4.4(链接用途) 原因:通用链接文本缺少上下文 预防措施:使用描述性链接文本或 aria-label

问题 #11:媒体自动播放

错误:视频/音频未经用户控制便自动播放 来源:WCAG 1.4.2(音频控制) 原因:使用 autoplay 属性,但未提供控制项 预防措施:要求用户交互后再开始播放媒体

问题 #12:自定义控件不可访问

错误:自定义选择框/复选框不支持键盘操作 来源:WCAG 4.1.2(名称、角色、值) 原因:使用 div 构建控件,却未添加 ARIA 预防措施:使用原生元素,或完整实现 ARIA 模式


WCAG 2.1 AA 快速检查清单

可感知性
  • 所有图像都有替代文本(装饰性图像使用 alt="")
  • 文本对比度 ≥ 4.5:1(普通文本),≥ 3:1(大号文本)
  • 不单独使用颜色传达信息
  • 文本可放大至 200%,且内容不会丢失
  • 不自动播放超过 3 秒的音频
可操作性
  • 所有功能均可通过键盘访问
  • 没有键盘陷阱
  • 焦点指示器清晰可见
  • 用户可以暂停/停止/隐藏移动内容
  • 页面标题能够说明用途
  • 焦点顺序合乎逻辑
  • 链接用途可从文本或上下文中明确理解
  • 有多种方式查找页面(菜单、搜索、站点地图)
  • 标题和标签能够说明用途
可理解性
  • 已指定页面语言(<html lang="en">)
  • 已标记语言变更(<span lang="es">)
  • 聚焦或输入时不会意外改变上下文
  • 全站导航保持一致
  • 提供表单标签/说明
  • 已识别并说明输入错误
  • 对法律、财务或数据变更提供错误预防机制
稳健性
  • HTML 有效(没有解析错误)
  • 所有 UI 组件均提供名称、角色和值
  • 已识别状态消息(aria-live)

测试工作流

1. 仅使用键盘进行测试(5 分钟)
1. Unplug mouse or hide cursor
2. Tab through entire page
   - Can you reach all interactive elements?
   - Can you activate all buttons/links?
   - Is focus order logical?
3. Use Enter/Space to activate
4. Use Escape to close dialogs
5. Use arrow keys in menus/tabs
2. 屏幕阅读器测试(10 分钟)

NVDA(Windows - 免费):

VoiceOver(Mac - 内置):

  • 启动:Cmd+F5
  • 导航:VO+向右/向左箭头(VO = Ctrl+Option)
  • 朗读:VO+A(朗读全部)
  • 停止:Cmd+F5

测试内容:

  • 所有交互元素是否都会被读出?
  • 图片是否都有恰当的描述?
  • 表单标签是否会与输入控件一起读出?
  • 动态更新是否会被读出?
  • 标题结构是否清晰?
3. 自动化测试

axe DevTools(浏览器扩展 - 强烈推荐):

  • 安装:Chrome/Firefox 扩展
  • 运行:F12 → axe DevTools 选项卡 → Scan
  • 修复:查看违规项并按照建议修复
  • 重新测试:修复后再次扫描

Lighthouse(Chrome 内置):

  • 打开 DevTools(F12)
  • Lighthouse 选项卡
  • 选择“Accessibility”类别
  • 生成报告
  • 得分达到 90+ 为良好,100 为理想

常见模式

模式 1:无障碍对话框/模态框
interface DialogProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

function Dialog({ isOpen, onClose, title, children }: DialogProps) {
  const dialogRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!isOpen) return;

    const previousFocus = document.activeElement as HTMLElement;

    // Focus first focusable element
    const firstFocusable = dialogRef.current?.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    ) as HTMLElement;
    firstFocusable?.focus();

    // Focus trap
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        onClose();
      }
      if (e.key === 'Tab') {
        const focusableElements = dialogRef.current?.querySelectorAll(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        if (!focusableElements?.length) return;

        const first = focusableElements[0] as HTMLElement;
        const last = focusableElements[focusableElements.length - 1] as HTMLElement;

        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      }
    };

    document.addEventListener('keydown', handleKeyDown);

    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      previousFocus?.focus();
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <>
      {/* Backdrop */}
      <div
        className="dialog-backdrop"
        onClick={onClose}
        aria-hidden="true"
      />

      {/* Dialog */}
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="dialog-title"
        className="dialog"
      >
        <h2 id="dialog-title">{title}</h2>
        <div className="dialog-content">{children}</div>
        <button onClick={onClose} aria-label="Close dialog">×</button>
      </div>
    </>
  );
}

适用场景:任何会阻止用户与背景内容交互的模态对话框或覆盖层。

模式 2:无障碍标签页
function Tabs({ tabs }: { tabs: Array<{ label: string; content: React.ReactNode }> }) {
  const [activeIndex, setActiveIndex] = useState(0);

  const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
    if (e.key === 'ArrowLeft') {
      e.preventDefault();
      const newIndex = index === 0 ? tabs.length - 1 : index - 1;
      setActiveIndex(newIndex);
    } else if (e.key === 'ArrowRight') {
      e.preventDefault();
      const newIndex = index === tabs.length - 1 ? 0 : index + 1;
      setActiveIndex(newIndex);
    } else if (e.key === 'Home') {
      e.preventDefault();
      setActiveIndex(0);
    } else if (e.key === 'End') {
      e.preventDefault();
      setActiveIndex(tabs.length - 1);
    }
  };

  return (
    <div>
      <div role="tablist" aria-label="Content tabs">
        {tabs.map((tab, index) => (
          <button
            key={index}
            role="tab"
            aria-selected={activeIndex === index}
            aria-controls={`panel-${index}`}
            id={`tab-${index}`}
            tabIndex={activeIndex === index ? 0 : -1}
            onClick={() => setActiveIndex(index)}
            onKeyDown={(e) => handleKeyDown(e, index)}
          >
            {tab.label}
          </button>
        ))}
      </div>
      {tabs.map((tab, index) => (
        <div
          key={index}
          role="tabpanel"
          id={`panel-${index}`}
          aria-labelledby={`tab-${index}`}
          hidden={activeIndex !== index}
          tabIndex={0}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

适用场景:包含多个面板的标签页界面。

模式 3:跳过链接

<a href="#main-content" class="skip-link">
  Skip to main content
</a>

<style>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--primary);
  color: white;
  padding: 8px 16px;
  z-index: 9999;
}

.skip-link:focus {
  top: 0;
}
</style>


<main id="main-content" tabindex="-1">
  
</main>

适用场景:所有主内容之前带有导航栏/页眉的多页面网站。

模式 4:带验证的无障碍表单
function ContactForm() {
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [touched, setTouched] = useState<Record<string, boolean>>({});

  const validateEmail = (email: string) => {
    if (!email) return 'Email is required';
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Email is invalid';
    return '';
  };

  const handleBlur = (field: string, value: string) => {
    setTouched(prev => ({ ...prev, [field]: true }));
    const error = validateEmail(value);
    setErrors(prev => ({ ...prev, [field]: error }));
  };

  return (
    <form>
      <div>
        <label htmlFor="email">Email address *</label>
        <input
          type="email"
          id="email"
          name="email"
          required
          aria-required="true"
          aria-invalid={touched.email && !!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
          onBlur={(e) => handleBlur('email', e.target.value)}
        />
        {touched.email && errors.email && (
          <span id="email-error" role="alert" className="error">
            {errors.email}
          </span>
        )}
      </div>

      <button type="submit">Submit</button>

      {/* Global form error */}
      <div role="alert" aria-live="assertive" aria-atomic="true">
        {/* Dynamic error message appears here */}
      </div>
    </form>
  );
}

适用场景:所有带验证的表单。


使用随附资源

参考文档(references/)

深入了解时可查阅的详细文档:

  • wcag-checklist.md - 完整的 WCAG 2.1 A 级和 AA 级要求及示例
  • semantic-html.md - 元素选择指南,以及何时使用各类标签
  • aria-patterns.md - ARIA 角色、状态、属性及其使用时机
  • focus-management.md - 焦点顺序、焦点陷阱、焦点恢复模式
  • color-contrast.md - 对比度要求、测试工具、调色板提示
  • forms-validation.md - 无障碍表单模式、错误处理、提示

Claude 应在以下情况下加载这些文档:

  • 用户要求提供完整的 WCAG 检查清单
  • 深入了解特定模式(标签页、手风琴等)
  • 颜色对比度问题或调色板设计
  • 复杂的表单验证场景
Agent(agents/)
  • a11y-auditor.md - 自动化无障碍审计工具,用于检查页面是否存在违规项

适用场景:用户请求对现有页面/组件进行无障碍审计。


高级主题

ARIA 实时区域

三个礼貌级别:


<div aria-live="polite">New messages: 3</div>


<div aria-live="assertive" role="alert">
  Error: Form submission failed
</div>


<div aria-live="off">Loading...</div>

最佳实践:

  • 对非关键更新(通知、计数器)使用 polite
  • 对错误和关键警报使用 assertive
  • 使用 aria-atomic="true",在区域发生变化时朗读整个区域
  • 保持提示简洁且有意义
单页应用中的焦点管理

React Router 不会在导航时重置焦点,因此需要自行处理:

function App() {
  const location = useLocation();
  const mainRef = useRef<HTMLElement>(null);

  useEffect(() => {
    // Focus main content on route change
    mainRef.current?.focus();
    // Announce page title to screen readers
    const title = document.title;
    const announcement = document.createElement('div');
    announcement.setAttribute('role', 'status');
    announcement.setAttribute('aria-live', 'polite');
    announcement.textContent = `Navigated to ${title}`;
    document.body.appendChild(announcement);
    setTimeout(() => announcement.remove(), 1000);
  }, [location.pathname]);

  return <main ref={mainRef} tabIndex={-1} id="main-content">...</main>;
}
无障碍数据表格
<table>
  <caption>Monthly sales by region</caption>
  <thead>
    <tr>
      <th scope="col">Region</th>
      <th scope="col">Q1</th>
      <th scope="col">Q2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">North</th>
      <td>$10,000</td>
      <td>$12,000</td>
    </tr>
  </tbody>
</table>

关键属性:

  • <caption> - 描述表格用途
  • scope="col" - 标识列标题
  • scope="row" - 标识行标题
  • 将数据单元格与标题关联,以便屏幕阅读器读取

官方文档


故障排除

问题:焦点指示器不可见

症状:可以使用 Tab 键浏览页面,但看不到焦点所在位置 原因:CSS 移除了轮廓,或对比度不足 解决方案:

*:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
问题:屏幕阅读器不播报更新

症状:动态内容发生变化,但没有播报 原因:没有 aria-live 区域 解决方案:将动态内容放入 <div aria-live="polite"> 中,或使用 role="alert"

问题:对话框焦点逃逸到背景

症状:按 Tab 键会导航到对话框后面的元素 原因:没有焦点陷阱 解决方案:实现焦点陷阱(参见上文模式 1)

问题:表单错误未被播报

症状:出现视觉错误,但屏幕阅读器未检测到 原因:缺少 aria-invalid 或 role="alert" 解决方案:使用 aria-invalid,并通过 aria-describedby 指向带有 role="alert" 的错误消息


完整设置清单

每个页面/组件都使用此清单:

  • 所有交互元素都可通过键盘访问
  • 所有可聚焦元素都有可见的焦点指示器
  • 图片都有替代文本(装饰性图片使用 alt="")
  • 文本对比度 ≥ 4.5:1(使用 axe 或 Lighthouse 测试)
  • 表单输入框都有对应的标签(不能只有占位符)
  • 标题层级符合逻辑(没有跳过层级)
  • 页面包含 <html lang="en"> 或适当的语言设置
  • 对话框设有焦点陷阱,并在关闭时恢复焦点
  • 动态内容使用 aria-live 或 role="alert"
  • 不单独依靠颜色传达信息
  • 仅使用键盘进行测试(不使用鼠标)
  • 使用屏幕阅读器进行测试(NVDA 或 VoiceOver)
  • 运行 axe DevTools 扫描(违规项为 0)
  • Lighthouse 无障碍评分 ≥ 90

有疑问?遇到问题?

  1. 查看 references/wcag-checklist.md 了解完整要求
  2. 使用 /a11y-auditor Agent 扫描页面
  3. 运行 axe DevTools 进行自动化测试
  4. 使用实体键盘和屏幕阅读器进行测试

标准:WCAG 2.1 AA 级 测试工具:axe DevTools、Lighthouse、NVDA、VoiceOver 成功标准:Lighthouse 评分达到 90 以上,严重违规项为 0

评论

支持 Markdown