mcpField
W
web-accessibility

web-accessibility

github.com/alexanderstephenthompson设计与 UI35 次安装
代码仓库

关于 web-accessibility

面向构建和重构 Web 界面的人员及 Agent,提供 WCAG 2.1 AA 无障碍实施规范。内容涵盖语义 HTML、键盘操作、ARIA、表单与测试,并提供检查清单和可确定性检查的规则。

主要功能

  1. 01WCAG 2.1 AA 实施指南
  2. 02语义 HTML 与 ARIA 模式
  3. 03键盘导航和焦点管理
  4. 04表单、对比度及媒体规范
  5. 05自动化与手动测试清单

适用场景

  1. 01编写新的 Web 界面
  2. 02检查并修复无障碍问题
  3. 03设计可键盘操作的模态框
  4. 04验证表单和动态内容的辅助技术支持

安装

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

$npx skills add https://github.com/alexanderstephenthompson/claude-hub --skill web-accessibility

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

详细介绍

Web 无障碍技能

版本: 1.0 标准: WCAG 2.1 AA 级

无障碍性不是可选项。这些模式可确保所有用户都能使用你的应用程序。

问题

AI Agent 构建的界面看起来正确,却并不具备无障碍性。没有明确的 WCAG 标准,每次会话都会漏掉替代文本、用 div 代替按钮、错误嵌套标题,以及忽略键盘导航——并非有意为之,而是因为这是阻力最小的做法。每次会话都会引入略有不同的无障碍缺陷,使得整改目标不断变化。这些标准可确保从第一个元素开始就构建无障碍体验。

使用方式

  • 构建者: 在编写任何 HTML 或组件标记之前,阅读 ## Builder Checklist。无障碍性必须在设计时融入,而不是事后通过 ARIA 修补。
  • 重构者: 使用 ## Enforced Rules 查找无障碍违规。阅读说明性章节,了解整改模式。
  • 两者: 说明性章节是权威标准。清单和规则表是同一内容的精简呈现。

范围与边界

本技能涵盖:

  • WCAG 2.1 AA 级合规
  • POUR 原则(可感知、可操作、可理解、健壮)
  • 用于无障碍体验的语义 HTML(地标、标题、按钮与链接)
  • 键盘导航和焦点管理
  • 模态框的焦点陷阱
  • ARIA 用法模式和角色
  • 表单无障碍(标签、错误、必填字段)
  • 颜色对比度要求
  • 图像和媒体的替代文本
  • 动态内容和实时区域
  • 减少动态效果支持
  • 屏幕阅读器测试

交由其他技能处理:

  • design:设计令牌系统、整体设计原则、组件状态
  • web-css:CSS 实现细节、文件组织、响应式断点

在以下情况下使用本技能: 你需要符合 WCAG、支持屏幕阅读器、管理焦点或使用 ARIA 模式。 在以下情况下使用 design: 你需要设计系统原则、令牌规范或布局理念。 在以下情况下使用 web-css: 你需要 CSS 架构或响应式实现。


核心原则(POUR)

  1. 可感知——用户能够感知所有内容(看见、听见或感受到)。
  2. 可操作——用户能够操作所有控件(键盘、鼠标、语音等)。
  3. 可理解——用户能够理解内容和界面行为。
  4. 健壮——内容可与当前及未来的辅助技术配合使用。

优先使用语义化 HTML

使用恰当的元素

<header>Site header</header>
<nav>Navigation</nav>
<main>
  <article>
    <h1>Article Title</h1>
    <p>Content...</p>
  </article>
  <aside>Related content</aside>
</main>
<footer>Site footer</footer>


<div class="header">Site header</div>
<div class="nav">Navigation</div>
<div class="main">
  <div class="article">
    <div class="title">Article Title</div>
    <div class="content">Content...</div>
  </div>
</div>
按钮与链接
// ✅ Button - Performs an action
<button onClick={handleSubmit}>Submit Form</button>
<button onClick={openModal}>Open Settings</button>

// ✅ Link - Navigates somewhere
<a href="/products">View Products</a>
<Link to="/checkout">Proceed to Checkout</Link>

// ❌ Bad - Wrong semantics
<div onClick={handleSubmit}>Submit Form</div>
<a onClick={openModal}>Open Settings</a>  {/* No href! */}
<button onClick={() => navigate('/products')}>View Products</button>
标题层级
// ✅ Good - Proper hierarchy
<h1>Page Title</h1>
<section>
  <h2>Section Title</h2>
  <h3>Subsection</h3>
</section>
<section>
  <h2>Another Section</h2>
</section>

// ❌ Bad - Skipped levels
<h1>Page Title</h1>
<h3>Subsection</h3>  {/* Skipped h2! */}
<h5>Deep section</h5> {/* Skipped h4! */}

键盘导航

焦点管理
/* Visible focus indicator */
:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* Remove outline only for mouse users */
:focus:not(:focus-visible) {
  outline: none;
}
Tab 顺序
// ✅ Good - Logical tab order (follows DOM order)
<form>
  <label htmlFor="email">Email</label>
  <input id="email" type="email" />

  <label htmlFor="password">Password</label>
  <input id="password" type="password" />

  <button type="submit">Login</button>
</form>

// ❌ Bad - Jumpy tab order
<form>
  <button type="submit" tabIndex={1}>Login</button>
  <input tabIndex={3} />
  <input tabIndex={2} />
</form>
跳过链接
// At the very top of your app
function SkipLink() {
  return (
    <a href="#main-content" className="skip-link">
      Skip to main content
    </a>
  );
}

// CSS
.skip-link {
  position: absolute;
  top: -100%;
  left: var(--space-4);
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface);
  z-index: var(--z-tooltip);
}

.skip-link:focus {
  top: var(--space-4);
}
键盘快捷键
// Listen for keyboard events
function SearchModal({ isOpen, onClose }) {
  useEffect(() => {
    function handleKeyDown(e) {
      if (e.key === 'Escape') {
        onClose();
      }
    }

    if (isOpen) {
      document.addEventListener('keydown', handleKeyDown);
      return () => document.removeEventListener('keydown', handleKeyDown);
    }
  }, [isOpen, onClose]);

  // ...
}

焦点陷阱(模态框)

模态框模式
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

function Modal({ isOpen, onClose, title, children }) {
  const modalRef = useRef(null);
  const previousFocus = useRef(null);

  useEffect(() => {
    if (isOpen) {
      // Store current focus
      previousFocus.current = document.activeElement;

      // Focus the modal
      modalRef.current?.focus();

      // Trap focus inside modal
      const modal = modalRef.current;
      const focusableElements = modal?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      const firstElement = focusableElements?.[0];
      const lastElement = focusableElements?.[focusableElements.length - 1];

      function handleTab(e) {
        if (e.key !== 'Tab') return;

        if (e.shiftKey && document.activeElement === firstElement) {
          e.preventDefault();
          lastElement?.focus();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          e.preventDefault();
          firstElement?.focus();
        }
      }

      modal?.addEventListener('keydown', handleTab);
      return () => modal?.removeEventListener('keydown', handleTab);
    } else {
      // Restore focus when closing
      previousFocus.current?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        className="modal"
        onClick={(e) => e.stopPropagation()}
        tabIndex={-1}
      >
        <h2 id="modal-title">{title}</h2>
        {children}
        <button onClick={onClose} aria-label="Close modal">
          ×
        </button>
      </div>
    </div>,
    document.body
  );
}

ARIA 用法

何时使用 ARIA

ARIA 的首要原则: 如果可以使用原生 HTML,就不要使用 ARIA。

// ❌ Unnecessary ARIA
<div role="button" tabIndex={0} onClick={handleClick}>
  Click me
</div>

// ✅ Just use a button
<button onClick={handleClick}>Click me</button>
必要的 ARIA 模式
// Labeling
<button aria-label="Close menu">×</button>
<input aria-labelledby="name-label helper-text" />

// Descriptions
<button aria-describedby="delete-warning">Delete Account</button>
<p id="delete-warning">This action cannot be undone.</p>

// States
<button aria-pressed={isActive}>Toggle</button>
<button aria-expanded={isOpen} aria-controls="menu">Menu</button>
<div id="menu" aria-hidden={!isOpen}>Menu content</div>

// Live regions (for dynamic content)
<div aria-live="polite" aria-atomic="true">
  {statusMessage}
</div>
常见 ARIA 角色
角色用例
alert重要消息(错误、警告)
alertdialog需要用户响应的模态框
dialog模态对话框
navigation导航区域
search搜索表单
tablist、tab、tabpanel选项卡界面
menu、menuitem下拉菜单
status状态更新(加载、保存)

表单

标签
// ✅ Explicit label association
<label htmlFor="email">Email Address</label>
<input id="email" type="email" />

// ✅ Implicit association (wrapped)
<label>
  Email Address
  <input type="email" />
</label>

// ❌ Bad - No association
<span>Email Address</span>
<input type="email" />
错误消息
function FormField({ id, label, error, ...props }) {
  const errorId = `${id}-error`;

  return (
    <div className="form-field">
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        aria-invalid={!!error}
        aria-describedby={error ? errorId : undefined}
        {...props}
      />
      {error && (
        <p id={errorId} className="error-message" role="alert">
          {error}
        </p>
      )}
    </div>
  );
}
必填字段
<label htmlFor="name">
  Name <span aria-hidden="true">*</span>
  <span className="visually-hidden">(required)</span>
</label>
<input id="name" required aria-required="true" />

颜色与对比度

最低对比度
文本类型比例示例
普通文本(< 18px)4.5:1正文
大号文本(≥ 18px 或 14px 粗体)3:1标题
UI 组件3:1按钮、输入框
不要只依赖颜色
// ❌ Bad - Color is only indicator
<span style={{ color: error ? 'red' : 'green' }}>
  {error ? 'Invalid' : 'Valid'}
</span>

// ✅ Good - Color + icon + text
<span className={error ? 'error' : 'success'}>
  {error ? (
    <>
      <ErrorIcon aria-hidden="true" /> Invalid: {error}
    </>
  ) : (
    <>
      <CheckIcon aria-hidden="true" /> Valid
    </>
  )}
</span>

图像与媒体

替代文本
// Informative image
<img src="chart.png" alt="Sales increased 40% from January to March" />

// Decorative image
<img src="divider.png" alt="" role="presentation" />

// Complex image
<figure>
  <img src="diagram.png" alt="Architecture diagram" aria-describedby="diagram-desc" />
  <figcaption id="diagram-desc">
    The system consists of three layers: presentation, business logic, and data.
    {/* Full description */}
  </figcaption>
</figure>
SVG 图标
// Decorative icon (with visible text)
<button>
  <SearchIcon aria-hidden="true" />
  Search
</button>

// Standalone icon (needs label)
<button aria-label="Search">
  <SearchIcon aria-hidden="true" />
</button>

// Icon with title
<svg role="img" aria-labelledby="icon-title">
  <title id="icon-title">Search</title>
  <path d="..." />
</svg>

动态内容

加载状态
function ProductList() {
  const { data, loading, error } = useQuery(GET_PRODUCTS);

  if (loading) {
    return (
      <div role="status" aria-live="polite">
        <Spinner aria-hidden="true" />
        <span className="visually-hidden">Loading products...</span>
      </div>
    );
  }

  if (error) {
    return (
      <div role="alert">
        Error loading products. Please try again.
      </div>
    );
  }

  return (
    <ul aria-label="Products">
      {data.products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}
实时区域
// Polite - Waits for user to finish current task
<div aria-live="polite" aria-atomic="true">
  {saveStatus} {/* "Saving...", "Saved!", etc. */}
</div>

// Assertive - Interrupts immediately (use sparingly)
<div aria-live="assertive" role="alert">
  {errorMessage}
</div>

减少动态效果

/* Respect user 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;
  }
}
// In React
function AnimatedComponent() {
  const prefersReducedMotion = window.matchMedia(
    '(prefers-reduced-motion: reduce)'
  ).matches;

  return (
    <motion.div
      animate={{ opacity: 1 }}
      transition={{
        duration: prefersReducedMotion ? 0 : 0.3,
      }}
    />
  );
}

视觉隐藏文本

.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;
}
// Use for screen reader only content
<button>
  <TrashIcon aria-hidden="true" />
  <span className="visually-hidden">Delete item</span>
</button>

<a href="/products">
  View all products
  <span className="visually-hidden"> in the catalog</span>
</a>

测试无障碍性

自动化测试
// jest + jest-axe
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<ProductCard product={mockProduct} />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
手动测试清单
  • 仅使用键盘浏览整个页面
  • 使用屏幕阅读器(VoiceOver、NVDA)
  • 缩放到 200% 后仍可正常使用?
  • 使用浏览器无障碍检查器检查
  • 使用高对比度模式测试
  • 启用减少动态效果后测试

反模式

反模式问题修复方法
所有内容都用 Div对辅助技术没有语义信息使用语义化 HTML
tabIndex > 0破坏自然的 Tab 顺序移除正数 tabIndex
Outline: none没有焦点指示器使用 :focus-visible
过度使用 ARIA复杂且容易出错优先使用原生 HTML
仅用颜色表达含义色盲用户无法识别添加图标和文字
媒体自动播放令人迷失方向、烦恼仅由用户主动触发
仅支持鼠标交互排斥键盘用户添加键盘事件处理程序
缺少 alt 文本屏幕阅读器无法识别图像添加描述或标记为装饰性内容
链接描述不明确“点击此处”毫无用处使用描述性链接文本

构建者清单

在编写受此技能约束的 UI 代码之前,请根据以下约束检查你的计划。构建者在编写代码前阅读本节;重构者则使用“强制规则”表和完整说明。

语义化 HTML
  • 标题层级正确
  • 操作用按钮,导航用链接
  • 使用语义化地标(header、nav、main、footer)
  • 列表使用 ul/ol/li
键盘
  • 所有交互元素均可获得焦点
  • 焦点指示器清晰可见
  • Tab 顺序符合逻辑
  • 提供跳过链接
  • 模态框限制焦点范围
屏幕阅读器
  • 所有图像均有 alt 文本
  • 表单输入项均有标签
  • 错误消息与输入项关联
  • 动态内容使用实时区域
  • 图标具有无障碍名称
视觉
  • 颜色对比度达到 4.5:1
  • 颜色不是唯一的指示方式
  • 在 200% 缩放下正常使用
  • 尊重减少动态效果设置
表单
  • 所有输入项均有标签
  • 标明必填字段
  • 清楚标识错误
  • 错误消息提供有用信息

强制规则

这些规则由 check.js(clean-team)进行确定性检查。更新这些标准时,请同步更新对应的 check.js 规则,反之亦然。

规则 ID严重性检查内容
img-alt-requirederror<img> 缺少 alt 属性
title-requirederror缺少 <title> 元素
tabindex-no-positiveerrortabindex 值为正数(会破坏 Tab 顺序)
heading-orderwarn标题级别跳跃(h1 → h3)
single-h1warn每页有多个 <h1> 元素
no-div-as-buttonwarn<div>/<span> 使用 onclick 处理程序

资源

评论

支持 Markdown