Skip to content

Web 无障碍设计指南

Web无障碍设计不仅是技术要求,更是社会责任。本文将深入探讨如何构建对所有用户都友好的Web应用,包括视觉、听觉、运动和认知障碍用户。

🎯 无障碍设计原则

WCAG 2.1 四大原则

1. 可感知性 (Perceivable)

  • 信息和UI组件必须以用户能够感知的方式呈现
  • 提供文本替代方案
  • 提供多媒体的替代方案
  • 确保内容可以以不同方式呈现而不丢失意义

2. 可操作性 (Operable)

  • UI组件和导航必须是可操作的
  • 所有功能都可通过键盘访问
  • 给用户足够的时间阅读和使用内容
  • 不使用会引起癫痫的内容

3. 可理解性 (Understandable)

  • 信息和UI操作必须是可理解的
  • 文本内容可读且可理解
  • 网页以可预测的方式出现和运行
  • 帮助用户避免和纠正错误

4. 健壮性 (Robust)

  • 内容必须足够健壮,能被各种用户代理解释
  • 最大化与辅助技术的兼容性

🔧 技术实现指南

语义化HTML

html
<!-- ✅ 正确的语义化结构 -->
<header>
  <nav aria-label="主导航">
    <ul>
      <li><a href="/" aria-current="page">首页</a></li>
      <li><a href="/about">关于我们</a></li>
      <li><a href="/contact">联系我们</a></li>
    </ul>
  </nav>
</header>

<main>
  <article>
    <header>
      <h1>文章标题</h1>
      <p>
        <time datetime="2025-06-15">2025年6月15日</time>
        由 <span>作者姓名</span> 发布
      </p>
    </header>
    
    <section>
      <h2>章节标题</h2>
      <p>章节内容...</p>
    </section>
  </article>
  
  <aside aria-label="相关文章">
    <h2>相关阅读</h2>
    <ul>
      <li><a href="/article1">相关文章1</a></li>
      <li><a href="/article2">相关文章2</a></li>
    </ul>
  </aside>
</main>

<footer>
  <p>&copy; 2025 网站名称. 保留所有权利.</p>
</footer>

<!-- ❌ 避免的非语义化结构 -->
<div class="header">
  <div class="nav">
    <div class="nav-item">首页</div>
    <div class="nav-item">关于</div>
  </div>
</div>

ARIA 属性应用

html
<!-- 表单无障碍 -->
<form>
  <fieldset>
    <legend>个人信息</legend>
    
    <div class="form-group">
      <label for="name">姓名 <span aria-label="必填">*</span></label>
      <input 
        type="text" 
        id="name" 
        name="name" 
        required 
        aria-describedby="name-help name-error"
        aria-invalid="false"
      />
      <div id="name-help" class="help-text">
        请输入您的真实姓名
      </div>
      <div id="name-error" class="error-text" aria-live="polite">
        <!-- 错误信息将在这里显示 -->
      </div>
    </div>
    
    <div class="form-group">
      <label for="email">邮箱地址 <span aria-label="必填">*</span></label>
      <input 
        type="email" 
        id="email" 
        name="email" 
        required 
        aria-describedby="email-help"
        autocomplete="email"
      />
      <div id="email-help" class="help-text">
        我们将使用此邮箱与您联系
      </div>
    </div>
  </fieldset>
  
  <button type="submit" aria-describedby="submit-help">
    提交表单
  </button>
  <div id="submit-help" class="help-text">
    点击提交将发送您的信息
  </div>
</form>

<!-- 交互组件无障碍 -->
<div class="dropdown">
  <button 
    aria-haspopup="true" 
    aria-expanded="false" 
    aria-controls="dropdown-menu"
    id="dropdown-button"
  >
    选择选项
    <span aria-hidden="true">▼</span>
  </button>
  
  <ul 
    id="dropdown-menu" 
    role="menu" 
    aria-labelledby="dropdown-button"
    hidden
  >
    <li role="menuitem">
      <a href="#" tabindex="-1">选项 1</a>
    </li>
    <li role="menuitem">
      <a href="#" tabindex="-1">选项 2</a>
    </li>
    <li role="menuitem">
      <a href="#" tabindex="-1">选项 3</a>
    </li>
  </ul>
</div>

<!-- 模态框无障碍 -->
<div 
  class="modal" 
  role="dialog" 
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
  aria-modal="true"
  hidden
>
  <div class="modal-content">
    <header class="modal-header">
      <h2 id="modal-title">确认删除</h2>
      <button 
        class="modal-close" 
        aria-label="关闭对话框"
        type="button"
      >
        <span aria-hidden="true">&times;</span>
      </button>
    </header>
    
    <div class="modal-body">
      <p id="modal-description">
        您确定要删除这个项目吗?此操作无法撤销。
      </p>
    </div>
    
    <footer class="modal-footer">
      <button type="button" class="btn-cancel">取消</button>
      <button type="button" class="btn-danger">删除</button>
    </footer>
  </div>
</div>

键盘导航支持

javascript
// 键盘导航管理器
class KeyboardNavigationManager {
  constructor() {
    this.focusableElements = [
      'a[href]',
      'button:not([disabled])',
      'input:not([disabled])',
      'select:not([disabled])',
      'textarea:not([disabled])',
      '[tabindex]:not([tabindex="-1"])'
    ].join(', ')
    
    this.init()
  }
  
  init() {
    document.addEventListener('keydown', this.handleKeyDown.bind(this))
    this.setupFocusManagement()
    this.setupSkipLinks()
  }
  
  // 处理键盘事件
  handleKeyDown(event) {
    switch (event.key) {
      case 'Tab':
        this.handleTabNavigation(event)
        break
      case 'Escape':
        this.handleEscapeKey(event)
        break
      case 'Enter':
      case ' ':
        this.handleActivation(event)
        break
      case 'ArrowUp':
      case 'ArrowDown':
      case 'ArrowLeft':
      case 'ArrowRight':
        this.handleArrowNavigation(event)
        break
    }
  }
  
  // Tab 导航处理
  handleTabNavigation(event) {
    const focusableElements = Array.from(
      document.querySelectorAll(this.focusableElements)
    ).filter(el => this.isVisible(el))
    
    const currentIndex = focusableElements.indexOf(document.activeElement)
    
    if (event.shiftKey) {
      // Shift + Tab (向后)
      if (currentIndex <= 0) {
        event.preventDefault()
        focusableElements[focusableElements.length - 1].focus()
      }
    } else {
      // Tab (向前)
      if (currentIndex >= focusableElements.length - 1) {
        event.preventDefault()
        focusableElements[0].focus()
      }
    }
  }
  
  // 焦点管理
  setupFocusManagement() {
    // 焦点陷阱 (用于模态框)
    this.createFocusTrap = (container) => {
      const focusableElements = container.querySelectorAll(this.focusableElements)
      const firstElement = focusableElements[0]
      const lastElement = focusableElements[focusableElements.length - 1]
      
      const trapFocus = (event) => {
        if (event.key === 'Tab') {
          if (event.shiftKey) {
            if (document.activeElement === firstElement) {
              event.preventDefault()
              lastElement.focus()
            }
          } else {
            if (document.activeElement === lastElement) {
              event.preventDefault()
              firstElement.focus()
            }
          }
        }
      }
      
      container.addEventListener('keydown', trapFocus)
      firstElement.focus()
      
      return () => {
        container.removeEventListener('keydown', trapFocus)
      }
    }
  }
  
  // 跳转链接设置
  setupSkipLinks() {
    const skipLink = document.createElement('a')
    skipLink.href = '#main-content'
    skipLink.textContent = '跳转到主要内容'
    skipLink.className = 'skip-link'
    
    // 样式
    const style = document.createElement('style')
    style.textContent = `
      .skip-link {
        position: absolute;
        top: -40px;
        left: 6px;
        background: #000;
        color: #fff;
        padding: 8px;
        text-decoration: none;
        border-radius: 4px;
        z-index: 1000;
        transition: top 0.3s;
      }
      
      .skip-link:focus {
        top: 6px;
      }
    `
    
    document.head.appendChild(style)
    document.body.insertBefore(skipLink, document.body.firstChild)
  }
  
  // 检查元素是否可见
  isVisible(element) {
    const style = window.getComputedStyle(element)
    return style.display !== 'none' && 
           style.visibility !== 'hidden' && 
           element.offsetParent !== null
  }
  
  // 箭头键导航 (用于菜单、列表等)
  handleArrowNavigation(event) {
    const target = event.target
    const parent = target.closest('[role="menu"], [role="listbox"], [role="tablist"]')
    
    if (!parent) return
    
    const items = Array.from(parent.querySelectorAll('[role="menuitem"], [role="option"], [role="tab"]'))
    const currentIndex = items.indexOf(target)
    
    let nextIndex
    
    switch (event.key) {
      case 'ArrowUp':
      case 'ArrowLeft':
        nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1
        break
      case 'ArrowDown':
      case 'ArrowRight':
        nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0
        break
    }
    
    if (nextIndex !== undefined) {
      event.preventDefault()
      items[nextIndex].focus()
    }
  }
}

// 初始化键盘导航
const keyboardNav = new KeyboardNavigationManager()

屏幕阅读器优化

javascript
// 屏幕阅读器公告管理器
class ScreenReaderAnnouncer {
  constructor() {
    this.createLiveRegions()
  }
  
  // 创建实时区域
  createLiveRegions() {
    // 礼貌公告区域 (不打断当前阅读)
    this.politeRegion = document.createElement('div')
    this.politeRegion.setAttribute('aria-live', 'polite')
    this.politeRegion.setAttribute('aria-atomic', 'true')
    this.politeRegion.className = 'sr-only'
    
    // 紧急公告区域 (立即公告)
    this.assertiveRegion = document.createElement('div')
    this.assertiveRegion.setAttribute('aria-live', 'assertive')
    this.assertiveRegion.setAttribute('aria-atomic', 'true')
    this.assertiveRegion.className = 'sr-only'
    
    // 状态区域
    this.statusRegion = document.createElement('div')
    this.statusRegion.setAttribute('role', 'status')
    this.statusRegion.setAttribute('aria-live', 'polite')
    this.statusRegion.className = 'sr-only'
    
    // 添加样式
    const style = document.createElement('style')
    style.textContent = `
      .sr-only {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
      }
    `
    
    document.head.appendChild(style)
    document.body.appendChild(this.politeRegion)
    document.body.appendChild(this.assertiveRegion)
    document.body.appendChild(this.statusRegion)
  }
  
  // 礼貌公告
  announce(message, priority = 'polite') {
    const region = priority === 'assertive' ? this.assertiveRegion : this.politeRegion
    
    // 清空后设置新消息
    region.textContent = ''
    setTimeout(() => {
      region.textContent = message
    }, 100)
    
    // 自动清空
    setTimeout(() => {
      region.textContent = ''
    }, 5000)
  }
  
  // 状态更新
  announceStatus(message) {
    this.statusRegion.textContent = message
    
    setTimeout(() => {
      this.statusRegion.textContent = ''
    }, 3000)
  }
  
  // 页面加载完成公告
  announcePageLoad(title) {
    this.announce(`页面已加载: ${title}`)
  }
  
  // 表单验证公告
  announceFormError(fieldName, error) {
    this.announce(`${fieldName}字段错误: ${error}`, 'assertive')
  }
  
  // 操作成功公告
  announceSuccess(message) {
    this.announceStatus(`成功: ${message}`)
  }
  
  // 加载状态公告
  announceLoading(message = '正在加载...') {
    this.announceStatus(message)
  }
}

// 使用示例
const announcer = new ScreenReaderAnnouncer()

// 表单提交示例
document.getElementById('contact-form').addEventListener('submit', async (event) => {
  event.preventDefault()
  
  announcer.announceLoading('正在提交表单...')
  
  try {
    const response = await submitForm(new FormData(event.target))
    announcer.announceSuccess('表单提交成功!')
  } catch (error) {
    announcer.announce('表单提交失败,请重试', 'assertive')
  }
})

🎨 视觉设计无障碍

颜色对比度

css
/* 确保足够的颜色对比度 */
:root {
  /* WCAG AA 级别对比度 4.5:1 */
  --text-primary: #212529;      /* 对比度 16.75:1 */
  --text-secondary: #6c757d;    /* 对比度 7.0:1 */
  --bg-primary: #ffffff;
  
  /* 链接颜色 */
  --link-color: #0066cc;        /* 对比度 7.0:1 */
  --link-hover: #004499;        /* 对比度 10.7:1 */
  
  /* 状态颜色 */
  --success: #28a745;           /* 对比度 4.5:1 */
  --warning: #856404;           /* 对比度 4.5:1 */
  --error: #dc3545;             /* 对比度 5.9:1 */
}

/* 高对比度模式支持 */
@media (prefers-contrast: high) {
  :root {
    --text-primary: #000000;
    --bg-primary: #ffffff;
    --link-color: #0000ee;
    --border-color: #000000;
  }
  
  .card {
    border: 2px solid var(--border-color);
  }
  
  button {
    border: 2px solid var(--text-primary);
  }
}

/* 减少动画偏好 */
@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;
  }
}

/* 焦点指示器 */
:focus {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

:focus:not(:focus-visible) {
  outline: none;
}

:focus-visible {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

/* 自定义焦点样式 */
.btn:focus-visible {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(0, 95, 204, 0.25);
}

.form-input:focus {
  border-color: #005fcc;
  box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.25);
}

响应式字体和间距

css
/* 可缩放的字体大小 */
html {
  font-size: 16px; /* 基础字体大小 */
}

/* 支持用户字体大小偏好 */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.5;
  font-size: 1rem;
}

/* 标题层次 */
h1 { font-size: 2.5rem; line-height: 1.2; }
h2 { font-size: 2rem; line-height: 1.3; }
h3 { font-size: 1.75rem; line-height: 1.3; }
h4 { font-size: 1.5rem; line-height: 1.4; }
h5 { font-size: 1.25rem; line-height: 1.4; }
h6 { font-size: 1.125rem; line-height: 1.4; }

/* 可点击区域最小尺寸 */
button,
a,
input[type="checkbox"],
input[type="radio"] {
  min-height: 44px;
  min-width: 44px;
}

/* 文本间距 */
p {
  margin-bottom: 1rem;
}

/* 列表间距 */
ul, ol {
  margin-bottom: 1rem;
  padding-left: 2rem;
}

li {
  margin-bottom: 0.25rem;
}

/* 表格无障碍 */
table {
  border-collapse: collapse;
  width: 100%;
}

th, td {
  border: 1px solid #dee2e6;
  padding: 0.75rem;
  text-align: left;
}

th {
  background-color: #f8f9fa;
  font-weight: 600;
}

/* 响应式设计 */
@media (max-width: 768px) {
  html {
    font-size: 14px;
  }
  
  .btn {
    min-height: 48px; /* 移动端更大的点击区域 */
    padding: 12px 16px;
  }
}

🧪 无障碍测试

自动化测试

javascript
// 无障碍测试工具
class AccessibilityTester {
  constructor() {
    this.violations = []
  }
  
  // 基础无障碍检查
  async runBasicChecks() {
    const checks = [
      this.checkImages(),
      this.checkHeadings(),
      this.checkForms(),
      this.checkLinks(),
      this.checkColorContrast(),
      this.checkKeyboardNavigation()
    ]
    
    const results = await Promise.all(checks)
    return this.generateReport(results)
  }
  
  // 检查图片替代文本
  checkImages() {
    const images = document.querySelectorAll('img')
    const violations = []
    
    images.forEach((img, index) => {
      if (!img.alt && !img.getAttribute('aria-label') && !img.getAttribute('aria-labelledby')) {
        violations.push({
          type: 'missing-alt-text',
          element: img,
          message: `图片 ${index + 1} 缺少替代文本`,
          severity: 'error'
        })
      }
      
      if (img.alt === img.src || img.alt === 'image') {
        violations.push({
          type: 'poor-alt-text',
          element: img,
          message: `图片 ${index + 1} 的替代文本质量较差`,
          severity: 'warning'
        })
      }
    })
    
    return violations
  }
  
  // 检查标题结构
  checkHeadings() {
    const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6')
    const violations = []
    let previousLevel = 0
    
    headings.forEach((heading, index) => {
      const level = parseInt(heading.tagName.charAt(1))
      
      if (index === 0 && level !== 1) {
        violations.push({
          type: 'heading-structure',
          element: heading,
          message: '页面应该以 h1 标题开始',
          severity: 'error'
        })
      }
      
      if (level - previousLevel > 1) {
        violations.push({
          type: 'heading-skip',
          element: heading,
          message: `标题级别跳跃:从 h${previousLevel} 跳到 h${level}`,
          severity: 'warning'
        })
      }
      
      previousLevel = level
    })
    
    return violations
  }
  
  // 检查表单无障碍
  checkForms() {
    const inputs = document.querySelectorAll('input, select, textarea')
    const violations = []
    
    inputs.forEach((input, index) => {
      const label = document.querySelector(`label[for="${input.id}"]`)
      const ariaLabel = input.getAttribute('aria-label')
      const ariaLabelledby = input.getAttribute('aria-labelledby')
      
      if (!label && !ariaLabel && !ariaLabelledby) {
        violations.push({
          type: 'missing-label',
          element: input,
          message: `表单控件 ${index + 1} 缺少标签`,
          severity: 'error'
        })
      }
      
      if (input.required && !input.getAttribute('aria-required')) {
        violations.push({
          type: 'missing-required-indicator',
          element: input,
          message: `必填字段 ${index + 1} 缺少 aria-required 属性`,
          severity: 'warning'
        })
      }
    })
    
    return violations
  }
  
  // 检查链接
  checkLinks() {
    const links = document.querySelectorAll('a')
    const violations = []
    
    links.forEach((link, index) => {
      const text = link.textContent.trim()
      const ariaLabel = link.getAttribute('aria-label')
      
      if (!text && !ariaLabel) {
        violations.push({
          type: 'empty-link',
          element: link,
          message: `链接 ${index + 1} 没有可访问的文本`,
          severity: 'error'
        })
      }
      
      if (['点击这里', '更多', '阅读更多'].includes(text.toLowerCase())) {
        violations.push({
          type: 'vague-link-text',
          element: link,
          message: `链接 ${index + 1} 的文本不够描述性`,
          severity: 'warning'
        })
      }
      
      if (link.target === '_blank' && !link.getAttribute('aria-label')?.includes('新窗口')) {
        violations.push({
          type: 'missing-new-window-indicator',
          element: link,
          message: `链接 ${index + 1} 在新窗口打开但没有提示`,
          severity: 'warning'
        })
      }
    })
    
    return violations
  }
  
  // 颜色对比度检查
  async checkColorContrast() {
    const violations = []
    const textElements = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, a, button, span, div')
    
    for (const element of textElements) {
      const styles = window.getComputedStyle(element)
      const color = styles.color
      const backgroundColor = styles.backgroundColor
      
      if (color && backgroundColor && backgroundColor !== 'rgba(0, 0, 0, 0)') {
        const contrast = this.calculateContrast(color, backgroundColor)
        
        if (contrast < 4.5) {
          violations.push({
            type: 'low-contrast',
            element,
            message: `文本对比度不足: ${contrast.toFixed(2)}:1`,
            severity: 'error'
          })
        }
      }
    }
    
    return violations
  }
  
  // 计算颜色对比度
  calculateContrast(color1, color2) {
    // 简化的对比度计算
    const rgb1 = this.parseColor(color1)
    const rgb2 = this.parseColor(color2)
    
    const l1 = this.getLuminance(rgb1)
    const l2 = this.getLuminance(rgb2)
    
    const lighter = Math.max(l1, l2)
    const darker = Math.min(l1, l2)
    
    return (lighter + 0.05) / (darker + 0.05)
  }
  
  // 解析颜色值
  parseColor(color) {
    const div = document.createElement('div')
    div.style.color = color
    document.body.appendChild(div)
    
    const computedColor = window.getComputedStyle(div).color
    document.body.removeChild(div)
    
    const match = computedColor.match(/rgb\((\d+), (\d+), (\d+)\)/)
    return match ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])] : [0, 0, 0]
  }
  
  // 计算亮度
  getLuminance([r, g, b]) {
    const [rs, gs, bs] = [r, g, b].map(c => {
      c = c / 255
      return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
    })
    
    return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
  }
  
  // 生成测试报告
  generateReport(results) {
    const allViolations = results.flat()
    const errorCount = allViolations.filter(v => v.severity === 'error').length
    const warningCount = allViolations.filter(v => v.severity === 'warning').length
    
    return {
      summary: {
        total: allViolations.length,
        errors: errorCount,
        warnings: warningCount,
        score: Math.max(0, 100 - (errorCount * 10 + warningCount * 5))
      },
      violations: allViolations
    }
  }
}

// 使用示例
const tester = new AccessibilityTester()
tester.runBasicChecks().then(report => {
  console.log('无障碍测试报告:', report)
  
  // 在控制台显示详细报告
  if (report.violations.length > 0) {
    console.group('无障碍问题详情:')
    report.violations.forEach((violation, index) => {
      console.log(`${index + 1}. ${violation.message}`)
      console.log('   元素:', violation.element)
      console.log('   严重程度:', violation.severity)
    })
    console.groupEnd()
  }
})

手动测试清单

markdown
## 无障碍手动测试清单

### 键盘导航测试
- [ ] 使用 Tab 键可以访问所有交互元素
- [ ] 焦点指示器清晰可见
- [ ] 焦点顺序符合逻辑
- [ ] 可以使用 Shift+Tab 反向导航
- [ ] 模态框和下拉菜单有焦点陷阱
- [ ] 可以使用 Escape 键关闭模态框

### 屏幕阅读器测试
- [ ] 页面标题准确描述内容
- [ ] 标题结构层次清晰
- [ ] 图片有适当的替代文本
- [ ] 表单控件有清晰的标签
- [ ] 链接文本具有描述性
- [ ] 状态变化会被正确公告

### 视觉测试
- [ ] 文本对比度符合 WCAG 标准
- [ ] 页面在 200% 缩放下仍可用
- [ ] 颜色不是传达信息的唯一方式
- [ ] 动画可以被暂停或禁用

### 认知无障碍测试
- [ ] 错误信息清晰易懂
- [ ] 表单有明确的提交反馈
- [ ] 复杂操作有帮助说明
- [ ] 用户有足够时间完成任务

🛠️ 实用工具和资源

开发工具

浏览器扩展:

  • axe DevTools - 自动化无障碍测试
  • WAVE - Web无障碍评估工具
  • Lighthouse - 包含无障碍审计
  • Color Contrast Analyzer - 颜色对比度检查

屏幕阅读器:

  • NVDA (Windows) - 免费开源
  • JAWS (Windows) - 商业软件
  • VoiceOver (macOS/iOS) - 系统内置
  • TalkBack (Android) - 系统内置

在线工具:

  • WebAIM Contrast Checker - 对比度检查
  • WAVE Web Accessibility Evaluator - 在线评估
  • Pa11y - 命令行无障碍测试工具

📱 移动端无障碍

iOS 无障碍

swift
// iOS VoiceOver 支持
import UIKit

class AccessibleViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setupAccessibility()
    }
    
    func setupAccessibility() {
        // 设置无障碍标签
        titleLabel.accessibilityLabel = "页面标题"
        titleLabel.accessibilityTraits = .header
        
        // 设置无障碍提示
        submitButton.accessibilityHint = "提交表单数据"
        
        // 自定义无障碍操作
        let customAction = UIAccessibilityCustomAction(
            name: "删除项目",
            target: self,
            selector: #selector(deleteItem)
        )
        itemView.accessibilityCustomActions = [customAction]
        
        // 设置无障碍容器
        containerView.shouldGroupAccessibilityChildren = true
        
        // 动态内容更新通知
        UIAccessibility.post(
            notification: .announcement,
            argument: "内容已更新"
        )
    }
    
    @objc func deleteItem() -> Bool {
        // 执行删除操作
        return true
    }
}

Android 无障碍

kotlin
// Android TalkBack 支持
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo

class AccessibleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setupAccessibility()
    }
    
    private fun setupAccessibility() {
        // 设置内容描述
        titleTextView.contentDescription = "页面标题"
        
        // 设置无障碍操作
        itemView.setAccessibilityDelegate(object : View.AccessibilityDelegate() {
            override fun onInitializeAccessibilityNodeInfo(
                host: View,
                info: AccessibilityNodeInfo
            ) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                
                // 添加自定义操作
                info.addAction(
                    AccessibilityNodeInfo.AccessibilityAction(
                        AccessibilityNodeInfo.ACTION_CLICK,
                        "删除项目"
                    )
                )
            }
        })
        
        // 发送无障碍事件
        val event = AccessibilityEvent.obtain(
            AccessibilityEvent.TYPE_ANNOUNCEMENT
        )
        event.text.add("内容已加载")
        accessibilityManager.sendAccessibilityEvent(event)
    }
}

🎯 最佳实践总结

设计阶段

  1. 包容性设计思维

    • 考虑不同能力用户的需求
    • 提供多种交互方式
    • 确保核心功能对所有用户可用
  2. 颜色和对比度

    • 使用高对比度颜色组合
    • 不仅依赖颜色传达信息
    • 提供高对比度模式选项
  3. 布局和导航

    • 保持一致的导航结构
    • 提供多种导航方式
    • 确保焦点顺序符合逻辑

开发阶段

  1. 语义化HTML

    • 使用正确的HTML元素
    • 提供适当的ARIA属性
    • 确保标题结构层次清晰
  2. 键盘支持

    • 所有功能都可通过键盘访问
    • 提供清晰的焦点指示器
    • 实现合理的焦点管理
  3. 屏幕阅读器优化

    • 提供有意义的替代文本
    • 使用实时区域公告状态变化
    • 确保内容结构清晰

测试阶段

  1. 自动化测试

    • 集成无障碍测试工具
    • 设置持续集成检查
    • 定期运行无障碍审计
  2. 手动测试

    • 使用键盘导航测试
    • 使用屏幕阅读器测试
    • 邀请残障用户参与测试
  3. 用户反馈

    • 提供无障碍反馈渠道
    • 及时响应用户问题
    • 持续改进无障碍体验

🔮 未来发展

新兴技术

  • AI辅助无障碍: 自动生成替代文本和描述
  • 语音交互: 更自然的语音控制界面
  • 眼动追踪: 为运动障碍用户提供新的交互方式
  • 触觉反馈: 增强移动设备的无障碍体验

标准演进

  • WCAG 3.0: 更全面的无障碍指南
  • 认知无障碍: 更多关注认知障碍用户需求
  • 移动无障碍: 针对移动设备的专门指南

Web无障碍不仅是技术要求,更是我们作为开发者的社会责任。通过遵循无障碍设计原则和最佳实践,我们可以创建真正包容所有用户的Web体验。


让我们共同努力,构建一个对所有人都友好的数字世界!

vitepress开发指南