网淘吧来吧,欢迎您!

Accessibility技能使用说明

2026-03-30 新闻来源:网淘吧 围观:24
电脑广告
手机广告

网站无障碍(WCAG 2.1 AA 级)

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


快速开始(5分钟)

1. 语义化 HTML 基础

选择正确的元素 - 不要在所有地方都使用div:

Accessibility

<!-- ❌ WRONG - divs with onClick -->
<div onclick="submit()">Submit</div>
<div onclick="navigate()">Next page</div>

<!-- ✅ CORRECT - semantic elements -->
<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. 文本替代方案

每个非文本元素都需要一个文本替代方案:

<!-- ❌ WRONG - no alt text -->
<img src="logo.png">
<button><svg>...</svg></button>

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

无障碍访问五步流程

第一步:选择语义化 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获取完整指南。

第二步:在需要时添加 ARIA

黄金法则:仅在 HTML 无法表达模式时使用 ARIA。

<!-- ❌ WRONG - unnecessary ARIA -->
<button role="button">Click me</button>  <!-- Button already has role -->

<!-- ✅ CORRECT - ARIA fills semantic gap -->
<div role="dialog" aria-labelledby="title" aria-modal="true">
  <h2 id="title">Confirm action</h2>
  <!-- No HTML dialog yet, so role needed -->
</div>

<!-- ✅ BETTER - Use native HTML when available -->
<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以获取完整模式。

步骤三:实现键盘导航

所有交互元素必须支持键盘访问:

// 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以获取完整模式。

步骤四:确保色彩对比度

WCAG AA 标准要求:

  • 常规文本(小于18磅):4.5:1 对比度比率
  • 大号文本(18磅以上或14磅加粗):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:确保表单可访问

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

<!-- ❌ WRONG - placeholder is not a label -->
<input type="email" placeholder="Email address">

<!-- ✅ CORRECT - proper label -->
<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用于动态内容更新

切勿

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


已知问题预防

此技能可预防12个有记录的可访问性问题:

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

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

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

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

问题 #3:缺少替代文本

错误: 图像缺少alt属性来源: WCAG 1.1.1(非文本内容)发生原因忘记添加或认为它是可选的预防措施:对于装饰性图像添加alt="",对于有意义的图像则添加描述性alt文本

问题 #4:键盘导航失效

错误:交互元素无法通过键盘访问来源:WCAG 2.1.1(键盘)发生原因:使用div的onClick事件而非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(音频控制)原因:使用了无控制功能的自动播放属性预防措施:要求用户交互以启动媒体

问题 #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标签 → 扫描
  • 修复:审查违规项,遵循修复建议
  • 重新测试:修复后再次扫描

Lighthouse(Chrome内置):

  • 打开开发者工具(F12)
  • Lighthouse标签
  • 选择“无障碍”类别
  • 生成报告
  • 90分以上为良好,100分为理想

常用模式

模式一:无障碍对话框/模态框

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>
    </>
  );
}

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

模式二:无障碍标签页

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>
  );
}

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

模式三:跳过链接

<!-- Place at very top of body -->
<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>

<!-- Then in your layout -->
<main id="main-content" tabindex="-1">
  <!-- Page content -->
</main>

使用场景:所有主内容前包含导航/页头的多页面网站。

模式四:带验证的无障碍表单

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检查清单
  • 深入探讨特定模式(标签页、手风琴组件等)
  • 色彩对比问题或调色板设计
  • 复杂表单验证场景

智能体(agents/)

  • a11y-auditor.md- 自动无障碍检测器,用于检查页面违规情况

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


高级主题

ARIA 实时区域

三种礼貌级别:

<!-- Polite: Wait for screen reader to finish current announcement -->
<div aria-live="polite">New messages: 3</div>

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

<!-- Off: Don't announce (default) -->
<div aria-live="off">Loading...</div>

最佳实践:

  • 使用礼貌用于非关键更新(通知、计数器)
  • 使用断言用于错误和关键警报
  • 使用aria-atomic="true"以在更改时读取整个区域
  • 保持消息简洁且有意义

SPA 中的焦点管理

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代理扫描您的页面
  3. 运行axe DevTools进行自动化测试
  4. 使用真实键盘和屏幕阅读器进行测试

标准:WCAG 2.1 AA级测试工具:axe DevTools、Lighthouse、NVDA、VoiceOver成功标准:Lighthouse评分90+,0个严重违规

免责申明
部分文章来自各大搜索引擎,如有侵权,请与我联系删除。
打赏
文章底部电脑广告
手机广告位-内容正文底部

相关文章

您是本站第334785名访客 今日有622篇新文章/评论