mcpField
A
accessibility-implementation

accessibility-implementation

github.com/laurigates设计与 UI5962 次安装1 天前更新
代码仓库

关于 accessibility-implementation

accessibility-implementation 是为开发者提供的无障碍实现技能,涵盖 WCAG、ARIA、键盘操作和屏幕阅读器支持。它也提供自动化及手动测试方法,帮助检查组件和页面的可访问性。

主要功能

  1. 01实现 WCAG 2.1/2.2 标准
  2. 02应用 ARIA 角色、状态和属性
  3. 03配置键盘导航与焦点管理
  4. 04支持屏幕阅读器内容播报
  5. 05使用自动化工具进行无障碍测试

适用场景

  1. 01为自定义组件添加键盘操作
  2. 02实现对话框焦点限制与恢复
  3. 03检查页面的无障碍违规
  4. 04改进表单标签和错误提示

安装

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

$npx skills add https://github.com/laurigates/claude-plugins --skill accessibility-implementation

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

详细介绍

无障碍功能实现

WCAG 指南、ARIA 模式和辅助技术支持的技术实现。

何时使用此技能

在以下情况下使用此技能……在以下情况下改用 design-tokens……
在代码中实现 WCAG 2.1/2.2 成功标准设置 CSS 自定义属性或主题系统
添加 ARIA 角色、状态或实时区域定义主题使用的语义颜色令牌
实现键盘导航、焦点陷阱或跳过链接组织基础/语义/组件令牌层级
使用 axe-core、jest-axe 或 Playwright 审核组件实现浅色/深色模式令牌覆盖

核心专业知识

  • WCAG 合规性:在代码中实现 WCAG 2.1/2.2 成功标准
  • ARIA 模式:正确使用角色、状态和属性
  • 键盘导航:焦点管理、按键处理器、合理的 Tab 顺序
  • 屏幕阅读器:内容结构、播报、实时区域
  • 测试:自动化和手动无障碍测试

WCAG 快速参考

A 级(必须具备)
标准实现方式
1.1.1 非文本内容为图像提供 alt,为输入控件提供标签
1.3.1 信息和关系语义化 HTML、ARIA 关系
2.1.1 键盘所有交互元素均可通过键盘访问
2.4.1 绕过区块跳过链接、地标
4.1.2 名称、角色、值自定义小组件的 ARIA 标签和角色
AA 级(应当具备)
标准实现方式
1.4.3 对比度(最低)文本 4.5:1,大号文本 3:1
1.4.11 非文本对比度UI 组件达到 3:1
2.4.6 标题和标签描述清晰、层级分明的标题
2.4.7 焦点可见可见的焦点指示器(2px 以上的轮廓)

ARIA 模式

按钮和链接

<div role="button" tabindex="0"
     aria-pressed="false"
     onkeydown="handleKeyDown(event)">
  Toggle Feature
</div>


<button aria-label="Close dialog">
  <svg aria-hidden="true">...</svg>
</button>



<a href="/page">Go to page</a>
<button type="button">Submit form</button>
表单控件

<label for="email">Email address</label>
<input id="email" type="email"
       aria-describedby="email-hint email-error"
       aria-invalid="true"
       required>
<div id="email-hint">We'll never share your email</div>
<div id="email-error" role="alert">Please enter a valid email</div>


<fieldset>
  <legend>Notification preferences</legend>
  <label><input type="checkbox" name="notif" value="email"> Email</label>
  <label><input type="checkbox" name="notif" value="sms"> SMS</label>
</fieldset>


<label for="country">Country</label>
<input id="country"
       role="combobox"
       aria-expanded="false"
       aria-autocomplete="list"
       aria-controls="country-listbox">
<ul id="country-listbox" role="listbox" hidden>
  <li role="option" id="opt-us">United States</li>
  <li role="option" id="opt-uk">United Kingdom</li>
</ul>
模态对话框
<div role="dialog"
     aria-modal="true"
     aria-labelledby="dialog-title"
     aria-describedby="dialog-desc">
  <h2 id="dialog-title">Confirm Action</h2>
  <p id="dialog-desc">Are you sure you want to proceed?</p>
  <button>Cancel</button>
  <button>Confirm</button>
</div>
// Focus trap implementation
function trapFocus(dialog: HTMLElement) {
  const focusable = dialog.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0] as HTMLElement;
  const last = focusable[focusable.length - 1] as HTMLElement;

  dialog.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
    if (e.key === 'Escape') {
      closeDialog();
    }
  });

  // Move focus to first element
  first.focus();
}
标签页
<div role="tablist" aria-label="Settings tabs">
  <button role="tab"
          id="tab-1"
          aria-selected="true"
          aria-controls="panel-1">
    General
  </button>
  <button role="tab"
          id="tab-2"
          aria-selected="false"
          aria-controls="panel-2"
          tabindex="-1">
    Privacy
  </button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  General settings content
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  Privacy settings content
</div>
// Tab keyboard navigation
tablist.addEventListener('keydown', (e) => {
  const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
  const current = tabs.indexOf(document.activeElement as Element);

  let next: number;
  switch (e.key) {
    case 'ArrowRight':
      next = (current + 1) % tabs.length;
      break;
    case 'ArrowLeft':
      next = (current - 1 + tabs.length) % tabs.length;
      break;
    case 'Home':
      next = 0;
      break;
    case 'End':
      next = tabs.length - 1;
      break;
    default:
      return;
  }

  e.preventDefault();
  (tabs[next] as HTMLElement).focus();
  activateTab(tabs[next]);
});
实时区域

<div role="status" aria-live="polite">
  Form saved successfully
</div>


<div role="alert" aria-live="assertive">
  Error: Connection lost
</div>


<div aria-live="polite" aria-atomic="true">
  Loading: 45% complete
</div>

键盘导航

标准按键绑定
按键行为
Tab移至下一个可聚焦元素
Shift+Tab移至上一个可聚焦元素
Enter/Space激活按钮、选择选项
Escape关闭模态框、取消操作
方向键在组件内导航(标签页、菜单、列表框)
Home/End移至列表中的第一项/最后一项
焦点管理
// Return focus after modal close
const triggerElement = document.activeElement;
openModal();
// On close:
closeModal();
triggerElement?.focus();

// Move focus to error
function showValidationErrors() {
  const firstError = document.querySelector('[aria-invalid="true"]');
  (firstError as HTMLElement)?.focus();
}

// Skip link
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content" tabindex="-1">...</main>
循环 Tabindex
// For composite widgets (toolbar, menu, tabs)
function setRovingTabindex(container: HTMLElement, selector: string) {
  const items = container.querySelectorAll(selector);

  items.forEach((item, index) => {
    item.setAttribute('tabindex', index === 0 ? '0' : '-1');
  });

  container.addEventListener('keydown', (e) => {
    const current = Array.from(items).indexOf(document.activeElement as Element);
    let next = current;

    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
      next = (current + 1) % items.length;
    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
      next = (current - 1 + items.length) % items.length;
    }

    if (next !== current) {
      items[current].setAttribute('tabindex', '-1');
      items[next].setAttribute('tabindex', '0');
      (items[next] as HTMLElement).focus();
      e.preventDefault();
    }
  });
}

测试

自动化测试
# axe-core CLI
npx @axe-core/cli https://localhost:3000

# Lighthouse accessibility audit
npx lighthouse http://localhost:3000 --only-categories=accessibility --output=json

# pa11y
npx pa11y http://localhost:3000

# jest-axe for unit tests
npm install --save-dev jest-axe
// jest-axe example
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('component is accessible', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
// Playwright accessibility testing
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('page should not have accessibility violations', async ({ page }) => {
  await page.goto('/');

  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .analyze();

  expect(results.violations).toEqual([]);
});
手动测试清单

键盘导航

  • 所有交互元素均可通过 Tab 访问
  • 焦点顺序与视觉顺序一致
  • 焦点指示器始终可见
  • 没有键盘陷阱
  • 按 Escape 可关闭模态框/菜单

屏幕阅读器测试

  • VoiceOver(macOS):Cmd+F5
  • NVDA(Windows):免费下载
  • 测试:链接会播报目标位置
  • 测试:表单会播报标签和错误
  • 测试:动态内容会被播报

视觉测试

  • 放大至 200% 时无需水平滚动
  • 颜色对比度符合比例要求
  • 信息不单靠颜色传达
  • 所有主题中的焦点指示器均清晰可见

常见修复方法

缺少可访问名称

<button><svg>...</svg></button>


<button aria-label="Close">
  <svg aria-hidden="true">...</svg>
</button>
缺少表单标签

<input placeholder="Email">


<label for="email">Email</label>
<input id="email" type="email">


<label for="search" class="visually-hidden">Search</label>
<input id="search" type="search" placeholder="Search...">
缺少标题结构

<h1>Page Title</h1>
<h3>Section</h3>  


<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
焦点不可见
/* Bad: Removing focus outline */
button:focus { outline: none; }

/* Good: Custom focus indicator */
button:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}
颜色对比度
/* Bad: Low contrast */
.text { color: #999; background: #fff; } /* 2.85:1 ratio */

/* Good: Sufficient contrast */
.text { color: #595959; background: #fff; } /* 4.56:1 ratio */

CSS 工具类

/* Visually hidden but accessible */
.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;
}

/* Skip link */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  padding: 8px;
  background: #000;
  color: #fff;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

最佳实践

优先使用语义化 HTML

优先使用原生 HTML 元素,而不是 ARIA。<button> 优于 <div role="button">。

不要覆盖默认行为

原生元素内置了无障碍功能。不要用 JavaScript 破坏这些功能。

与真实用户一起测试

自动化工具只能发现约 30% 的问题。使用辅助技术进行手动测试至关重要。

提供多种方式

为所有交互提供键盘、鼠标和触控替代方式。

References

评论

支持 Markdown