accessibility-compliance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Accessibility Compliance

无障碍合规性

Overview

概述

Implement comprehensive accessibility features following WCAG guidelines to ensure your application is usable by everyone, including people with disabilities.
遵循WCAG指南实现全面的无障碍功能,确保你的应用可供所有人使用,包括残障人士。

When to Use

适用场景

  • Building public-facing web applications
  • Ensuring WCAG 2.1/2.2 AA or AAA compliance
  • Supporting screen readers (NVDA, JAWS, VoiceOver)
  • Implementing keyboard-only navigation
  • Meeting ADA, Section 508, or similar regulations
  • Improving SEO and overall user experience
  • Conducting accessibility audits
  • 构建面向公众的Web应用
  • 确保符合WCAG 2.1/2.2 AA或AAA级别标准
  • 支持屏幕阅读器(NVDA、JAWS、VoiceOver)
  • 实现纯键盘导航
  • 满足ADA、Section 508或类似法规要求
  • 提升SEO及整体用户体验
  • 开展无障碍审计

Key Principles (POUR)

核心原则(POUR)

  1. Perceivable - Information must be presentable to users in ways they can perceive
  2. Operable - Interface components must be operable
  3. Understandable - Information and operation must be understandable
  4. Robust - Content must be robust enough to be interpreted by assistive technologies
  1. 可感知 - 信息必须以用户可感知的方式呈现
  2. 可操作 - 界面组件必须具备可操作性
  3. 可理解 - 信息及操作方式必须易于理解
  4. 健壮性 - 内容必须足够健壮,能被辅助技术解析

Implementation Examples

实现示例

1. Semantic HTML with ARIA

1. 使用ARIA的语义化HTML

html
<!-- Bad: Non-semantic markup -->
<div class="button" onclick="submit()">Submit</div>

<!-- Good: Semantic HTML -->
<button type="submit" aria-label="Submit form">Submit</button>

<!-- Custom components with proper ARIA -->
<div
  role="button"
  tabindex="0"
  aria-pressed="false"
  onclick="toggle()"
  onkeydown="handleKeyPress(event)"
>
  Toggle Feature
</div>

<!-- Form with proper labels and error handling -->
<form>
  <label for="email">Email Address</label>
  <input
    id="email"
    type="email"
    name="email"
    aria-required="true"
    aria-invalid="false"
    aria-describedby="email-error"
  />
  <span id="email-error" role="alert" aria-live="polite"></span>
</form>
html
<!-- Bad: Non-semantic markup -->
<div class="button" onclick="submit()">Submit</div>

<!-- Good: Semantic HTML -->
<button type="submit" aria-label="Submit form">Submit</button>

<!-- Custom components with proper ARIA -->
<div
  role="button"
  tabindex="0"
  aria-pressed="false"
  onclick="toggle()"
  onkeydown="handleKeyPress(event)"
>
  Toggle Feature
</div>

<!-- Form with proper labels and error handling -->
<form>
  <label for="email">Email Address</label>
  <input
    id="email"
    type="email"
    name="email"
    aria-required="true"
    aria-invalid="false"
    aria-describedby="email-error"
  />
  <span id="email-error" role="alert" aria-live="polite"></span>
</form>

2. React Component with Accessibility

2. 具备无障碍特性的React组件

typescript
import React, { useRef, useEffect, useState } from 'react';

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

const AccessibleModal: React.FC<ModalProps> = ({
  isOpen,
  onClose,
  title,
  children
}) => {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

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

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

      // Trap focus within modal
      const trapFocus = (e: KeyboardEvent) => {
        if (e.key === 'Escape') {
          onClose();
        }
      };

      document.addEventListener('keydown', trapFocus);

      return () => {
        document.removeEventListener('keydown', trapFocus);
        // Restore previous focus
        previousFocusRef.current?.focus();
      };
    }
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      ref={modalRef}
      tabIndex={-1}
      className="modal-overlay"
      onClick={onClose}
    >
      <div
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
      >
        <h2 id="modal-title">{title}</h2>
        <button
          onClick={onClose}
          aria-label="Close modal"
          className="close-button"
        >
          &times;
        </button>
        <div className="modal-body">
          {children}
        </div>
      </div>
    </div>
  );
};

export default AccessibleModal;
typescript
import React, { useRef, useEffect, useState } from 'react';

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

const AccessibleModal: React.FC<ModalProps> = ({
  isOpen,
  onClose,
  title,
  children
}) => {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

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

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

      // Trap focus within modal
      const trapFocus = (e: KeyboardEvent) => {
        if (e.key === 'Escape') {
          onClose();
        }
      };

      document.addEventListener('keydown', trapFocus);

      return () => {
        document.removeEventListener('keydown', trapFocus);
        // Restore previous focus
        previousFocusRef.current?.focus();
      };
    }
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      ref={modalRef}
      tabIndex={-1}
      className="modal-overlay"
      onClick={onClose}
    >
      <div
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
      >
        <h2 id="modal-title">{title}</h2>
        <button
          onClick={onClose}
          aria-label="Close modal"
          className="close-button"
        >
          &times;
        </button>
        <div className="modal-body">
          {children}
        </div>
      </div>
    </div>
  );
};

export default AccessibleModal;

3. Keyboard Navigation Handler

3. 键盘导航处理器

typescript
// Keyboard navigation utilities
export const KeyboardNavigation = {
  // Handle arrow key navigation in lists
  handleListNavigation: (event: KeyboardEvent, items: HTMLElement[]) => {
    const currentIndex = items.findIndex(item =>
      item === document.activeElement
    );

    let nextIndex: number;

    switch (event.key) {
      case 'ArrowDown':
        event.preventDefault();
        nextIndex = Math.min(currentIndex + 1, items.length - 1);
        items[nextIndex]?.focus();
        break;

      case 'ArrowUp':
        event.preventDefault();
        nextIndex = Math.max(currentIndex - 1, 0);
        items[nextIndex]?.focus();
        break;

      case 'Home':
        event.preventDefault();
        items[0]?.focus();
        break;

      case 'End':
        event.preventDefault();
        items[items.length - 1]?.focus();
        break;
    }
  },

  // Make element keyboard accessible
  makeAccessible: (
    element: HTMLElement,
    onClick: () => void
  ): void => {
    element.setAttribute('tabindex', '0');
    element.setAttribute('role', 'button');

    element.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        onClick();
      }
    });
  }
};
typescript
// Keyboard navigation utilities
export const KeyboardNavigation = {
  // Handle arrow key navigation in lists
  handleListNavigation: (event: KeyboardEvent, items: HTMLElement[]) => {
    const currentIndex = items.findIndex(item =>
      item === document.activeElement
    );

    let nextIndex: number;

    switch (event.key) {
      case 'ArrowDown':
        event.preventDefault();
        nextIndex = Math.min(currentIndex + 1, items.length - 1);
        items[nextIndex]?.focus();
        break;

      case 'ArrowUp':
        event.preventDefault();
        nextIndex = Math.max(currentIndex - 1, 0);
        items[nextIndex]?.focus();
        break;

      case 'Home':
        event.preventDefault();
        items[0]?.focus();
        break;

      case 'End':
        event.preventDefault();
        items[items.length - 1]?.focus();
        break;
    }
  },

  // Make element keyboard accessible
  makeAccessible: (
    element: HTMLElement,
    onClick: () => void
  ): void => {
    element.setAttribute('tabindex', '0');
    element.setAttribute('role', 'button');

    element.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        onClick();
      }
    });
  }
};

4. Color Contrast Validator

4. 色彩对比度验证器

python
from typing import Tuple
import math

def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
    """Convert hex color to RGB."""
    hex_color = hex_color.lstrip('#')
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

def calculate_luminance(rgb: Tuple[int, int, int]) -> float:
    """Calculate relative luminance."""
    def adjust(color: int) -> float:
        c = color / 255.0
        if c <= 0.03928:
            return c / 12.92
        return math.pow((c + 0.055) / 1.055, 2.4)

    r, g, b = rgb
    return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b)

def calculate_contrast_ratio(color1: str, color2: str) -> float:
    """Calculate WCAG contrast ratio between two colors."""
    lum1 = calculate_luminance(hex_to_rgb(color1))
    lum2 = calculate_luminance(hex_to_rgb(color2))

    lighter = max(lum1, lum2)
    darker = min(lum1, lum2)

    return (lighter + 0.05) / (darker + 0.05)

def check_wcag_compliance(
    foreground: str,
    background: str,
    level: str = 'AA',
    large_text: bool = False
) -> dict:
    """Check if color combination meets WCAG standards."""
    ratio = calculate_contrast_ratio(foreground, background)

    # WCAG 2.1 requirements
    requirements = {
        'AA': {'normal': 4.5, 'large': 3.0},
        'AAA': {'normal': 7.0, 'large': 4.5}
    }

    required_ratio = requirements[level]['large' if large_text else 'normal']
    passes = ratio >= required_ratio

    return {
        'ratio': round(ratio, 2),
        'required': required_ratio,
        'passes': passes,
        'level': level,
        'grade': 'Pass' if passes else 'Fail'
    }
python
from typing import Tuple
import math

def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
    """Convert hex color to RGB."""
    hex_color = hex_color.lstrip('#')
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

def calculate_luminance(rgb: Tuple[int, int, int]) -> float:
    """Calculate relative luminance."""
    def adjust(color: int) -> float:
        c = color / 255.0
        if c <= 0.03928:
            return c / 12.92
        return math.pow((c + 0.055) / 1.055, 2.4)

    r, g, b = rgb
    return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b)

def calculate_contrast_ratio(color1: str, color2: str) -> float:
    """Calculate WCAG contrast ratio between two colors."""
    lum1 = calculate_luminance(hex_to_rgb(color1))
    lum2 = calculate_luminance(hex_to_rgb(color2))

    lighter = max(lum1, lum2)
    darker = min(lum1, lum2)

    return (lighter + 0.05) / (darker + 0.05)

def check_wcag_compliance(
    foreground: str,
    background: str,
    level: str = 'AA',
    large_text: bool = False
) -> dict:
    """Check if color combination meets WCAG standards."""
    ratio = calculate_contrast_ratio(foreground, background)

    # WCAG 2.1 requirements
    requirements = {
        'AA': {'normal': 4.5, 'large': 3.0},
        'AAA': {'normal': 7.0, 'large': 4.5}
    }

    required_ratio = requirements[level]['large' if large_text else 'normal']
    passes = ratio >= required_ratio

    return {
        'ratio': round(ratio, 2),
        'required': required_ratio,
        'passes': passes,
        'level': level,
        'grade': 'Pass' if passes else 'Fail'
    }

Usage

Usage

result = check_wcag_compliance('#000000', '#FFFFFF', 'AA', False) print(f"Contrast ratio: {result['ratio']}:1") # 21:1 print(f"WCAG {result['level']}: {result['grade']}") # Pass
undefined
result = check_wcag_compliance('#000000', '#FFFFFF', 'AA', False) print(f"Contrast ratio: {result['ratio']}:1") # 21:1 print(f"WCAG {result['level']}: {result['grade']}") # Pass
undefined

5. Screen Reader Announcements

5. 屏幕阅读器播报器

typescript
class ScreenReaderAnnouncer {
  private liveRegion: HTMLElement;

  constructor() {
    this.liveRegion = this.createLiveRegion();
  }

  private createLiveRegion(): HTMLElement {
    const region = document.createElement('div');
    region.setAttribute('role', 'status');
    region.setAttribute('aria-live', 'polite');
    region.setAttribute('aria-atomic', 'true');
    region.className = 'sr-only';
    region.style.cssText = `
      position: absolute;
      left: -10000px;
      width: 1px;
      height: 1px;
      overflow: hidden;
    `;
    document.body.appendChild(region);
    return region;
  }

  announce(message: string, priority: 'polite' | 'assertive' = 'polite'): void {
    this.liveRegion.setAttribute('aria-live', priority);

    // Clear then set message to ensure announcement
    this.liveRegion.textContent = '';
    setTimeout(() => {
      this.liveRegion.textContent = message;
    }, 100);
  }

  cleanup(): void {
    this.liveRegion.remove();
  }
}

// Usage
const announcer = new ScreenReaderAnnouncer();

// Announce form validation error
announcer.announce('Email field is required', 'assertive');

// Announce successful action
announcer.announce('Item added to cart', 'polite');
typescript
class ScreenReaderAnnouncer {
  private liveRegion: HTMLElement;

  constructor() {
    this.liveRegion = this.createLiveRegion();
  }

  private createLiveRegion(): HTMLElement {
    const region = document.createElement('div');
    region.setAttribute('role', 'status');
    region.setAttribute('aria-live', 'polite');
    region.setAttribute('aria-atomic', 'true');
    region.className = 'sr-only';
    region.style.cssText = `
      position: absolute;
      left: -10000px;
      width: 1px;
      height: 1px;
      overflow: hidden;
    `;
    document.body.appendChild(region);
    return region;
  }

  announce(message: string, priority: 'polite' | 'assertive' = 'polite'): void {
    this.liveRegion.setAttribute('aria-live', priority);

    // Clear then set message to ensure announcement
    this.liveRegion.textContent = '';
    setTimeout(() => {
      this.liveRegion.textContent = message;
    }, 100);
  }

  cleanup(): void {
    this.liveRegion.remove();
  }
}

// Usage
const announcer = new ScreenReaderAnnouncer();

// Announce form validation error
announcer.announce('Email field is required', 'assertive');

// Announce successful action
announcer.announce('Item added to cart', 'polite');

6. Focus Management

6. 焦点管理器

typescript
class FocusManager {
  private focusableSelectors = [
    'a[href]',
    'button:not([disabled])',
    'textarea:not([disabled])',
    'input:not([disabled])',
    'select:not([disabled])',
    '[tabindex]:not([tabindex="-1"])'
  ].join(', ');

  getFocusableElements(container: HTMLElement): HTMLElement[] {
    return Array.from(
      container.querySelectorAll(this.focusableSelectors)
    ) as HTMLElement[];
  }

  trapFocus(container: HTMLElement): () => void {
    const focusable = this.getFocusableElements(container);
    const firstFocusable = focusable[0];
    const lastFocusable = focusable[focusable.length - 1];

    const handleTabKey = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return;

      if (e.shiftKey) {
        if (document.activeElement === firstFocusable) {
          lastFocusable.focus();
          e.preventDefault();
        }
      } else {
        if (document.activeElement === lastFocusable) {
          firstFocusable.focus();
          e.preventDefault();
        }
      }
    };

    container.addEventListener('keydown', handleTabKey);

    return () => container.removeEventListener('keydown', handleTabKey);
  }
}
typescript
class FocusManager {
  private focusableSelectors = [
    'a[href]',
    'button:not([disabled])',
    'textarea:not([disabled])',
    'input:not([disabled])',
    'select:not([disabled])',
    '[tabindex]:not([tabindex="-1"])'
  ].join(', ');

  getFocusableElements(container: HTMLElement): HTMLElement[] {
    return Array.from(
      container.querySelectorAll(this.focusableSelectors)
    ) as HTMLElement[];
  }

  trapFocus(container: HTMLElement): () => void {
    const focusable = this.getFocusableElements(container);
    const firstFocusable = focusable[0];
    const lastFocusable = focusable[focusable.length - 1];

    const handleTabKey = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return;

      if (e.shiftKey) {
        if (document.activeElement === firstFocusable) {
          lastFocusable.focus();
          e.preventDefault();
        }
      } else {
        if (document.activeElement === lastFocusable) {
          firstFocusable.focus();
          e.preventDefault();
        }
      }
    };

    container.addEventListener('keydown', handleTabKey);

    return () => container.removeEventListener('keydown', handleTabKey);
  }
}

Testing Tools and Techniques

测试工具与方法

Automated Testing

自动化测试

typescript
// Jest + Testing Library accessibility tests
import { render, screen } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

describe('Accessibility', () => {
  it('should not have accessibility violations', async () => {
    const { container } = render(<MyComponent />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it('should have proper ARIA labels', () => {
    render(<Button onClick={() => {}}>Click me</Button>);
    const button = screen.getByRole('button', { name: /click me/i });
    expect(button).toBeInTheDocument();
  });

  it('should be keyboard navigable', () => {
    const { container } = render(<Navigation />);
    const links = screen.getAllByRole('link');
    links.forEach(link => {
      expect(link).toHaveAttribute('href');
    });
  });
});
typescript
// Jest + Testing Library accessibility tests
import { render, screen } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

describe('Accessibility', () => {
  it('should not have accessibility violations', async () => {
    const { container } = render(<MyComponent />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it('should have proper ARIA labels', () => {
    render(<Button onClick={() => {}}>Click me</Button>);
    const button = screen.getByRole('button', { name: /click me/i });
    expect(button).toBeInTheDocument();
  });

  it('should be keyboard navigable', () => {
    const { container } = render(<Navigation />);
    const links = screen.getAllByRole('link');
    links.forEach(link => {
      expect(link).toHaveAttribute('href');
    });
  });
});

Best Practices

最佳实践

✅ DO

✅ 建议

  • Use semantic HTML elements
  • Provide text alternatives for images
  • Ensure sufficient color contrast (4.5:1 minimum)
  • Support keyboard navigation
  • Implement focus management
  • Test with screen readers
  • Use ARIA attributes correctly
  • Provide skip links
  • Make forms accessible with labels
  • Support text resizing up to 200%
  • 使用语义化HTML元素
  • 为图片提供文本替代内容
  • 确保足够的色彩对比度(最低4.5:1)
  • 支持键盘导航
  • 实现焦点管理
  • 使用屏幕阅读器进行测试
  • 正确使用ARIA属性
  • 提供跳转链接
  • 为表单添加无障碍标签
  • 支持文本放大至200%

❌ DON'T

❌ 避免

  • Rely solely on color to convey information
  • Remove focus indicators
  • Use only mouse/touch interactions
  • Auto-play media without controls
  • Create keyboard traps
  • Use positive tabindex values
  • Override user preferences
  • Hide content only visually that should be hidden from screen readers
  • 仅依赖颜色传递信息
  • 移除焦点指示器
  • 仅使用鼠标/触摸交互
  • 无控制项的自动播放媒体
  • 创建键盘陷阱
  • 使用正的tabindex值
  • 覆盖用户偏好设置
  • 仅隐藏视觉内容但对屏幕阅读器可见

Checklist

检查清单

  • All images have alt text
  • Color contrast meets WCAG AA standards
  • All interactive elements are keyboard accessible
  • Focus indicators are visible
  • Form inputs have associated labels
  • Error messages are announced to screen readers
  • Skip links are provided
  • Headings follow hierarchical order
  • ARIA attributes are used correctly
  • Content is readable at 200% zoom
  • Tested with keyboard only
  • Tested with screen reader (NVDA, JAWS, VoiceOver)
  • 所有图片均有替代文本
  • 色彩对比度符合WCAG AA标准
  • 所有交互元素支持键盘操作
  • 焦点指示器可见
  • 表单输入项关联对应标签
  • 错误消息可被屏幕阅读器播报
  • 提供跳转链接
  • 标题遵循层级结构
  • ARIA属性使用正确
  • 内容在200%缩放时仍可读
  • 仅使用键盘完成测试
  • 使用屏幕阅读器测试(NVDA、JAWS、VoiceOver)

Resources

参考资源