网络可访问性Skill web-accessibility

这个技能专注于通过实现WCAG标准、ARIA模式、键盘导航和屏幕阅读器支持,来构建可访问的网络应用程序,确保所有用户,包括残障人士,都能平等访问网络内容。关键词:网络可访问性、WCAG、ARIA、前端开发、无障碍设计、SEO可访问性、键盘导航、屏幕阅读器。

前端开发 0 次安装 0 次浏览 更新于 3/22/2026

name: 网络可访问性 description: 构建遵循WCAG指南的可访问网络应用程序。用于实现ARIA模式、键盘导航、屏幕阅读器支持或确保可访问性合规。触发词:可访问性、a11y、WCAG、ARIA、屏幕阅读器、键盘导航。

网络可访问性 (WCAG 2.1)

构建对所有人可访问的网络应用程序。

ARIA模式

按钮

<button
  type="button"
  aria-pressed={isPressed}
  aria-disabled={isDisabled}
  onClick={handleClick}
>
  切换功能
</button>

模态对话框

<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
>
  <h2 id="modal-title">确认操作</h2>
  <p id="modal-description">您确定要继续吗?</p>
  <button onClick={onConfirm}>确认</button>
  <button onClick={onCancel}>取消</button>
</div>

导航菜单

<nav aria-label="主导航">
  <ul role="menubar">
    <li role="none">
      <a role="menuitem" href="/home">首页</a>
    </li>
    <li role="none">
      <button
        role="menuitem"
        aria-haspopup="true"
        aria-expanded={isOpen}
      >
        产品
      </button>
      {isOpen && (
        <ul role="menu" aria-label="产品子菜单">
          <li role="none">
            <a role="menuitem" href="/products/new">新建</a>
          </li>
        </ul>
      )}
    </li>
  </ul>
</nav>

键盘导航

焦点管理

import { useEffect, useRef } from 'react';

function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocus = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      previousFocus.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      previousFocus.current?.focus();
    }
  }, [isOpen]);

  // 在模态框内捕获焦点
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Escape') {
      onClose();
    }
    
    if (e.key === 'Tab') {
      const focusable = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      
      if (focusable && focusable.length > 0) {
        const first = focusable[0] as HTMLElement;
        const last = focusable[focusable.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();
        }
      }
    }
  };

  if (!isOpen) return null;

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      tabIndex={-1}
      onKeyDown={handleKeyDown}
    >
      {children}
    </div>
  );
}

颜色对比度

最小对比度比(WCAG AA):

  • 正常文本:4.5:1
  • 大文本(18点以上):3:1
  • UI组件:3:1
function getContrastRatio(color1: string, color2: string): number {
  const lum1 = getLuminance(color1);
  const lum2 = getLuminance(color2);
  const lighter = Math.max(lum1, lum2);
  const darker = Math.min(lum1, lum2);
  return (lighter + 0.05) / (darker + 0.05);
}

function getLuminance(hex: string): number {
  const rgb = hexToRgb(hex);
  const [r, g, b] = rgb.map((c) => {
    c = c / 255;
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

可访问表单

<form onSubmit={handleSubmit}>
  <div>
    <label htmlFor="email">
      电子邮件地址
      <span aria-hidden="true">*</span>
      <span className="sr-only">(必填)</span>
    </label>
    <input
      id="email"
      type="email"
      aria-required="true"
      aria-invalid={errors.email ? 'true' : 'false'}
      aria-describedby={errors.email ? 'email-error' : undefined}
    />
    {errors.email && (
      <p id="email-error" role="alert" className="error">
        {errors.email}
      </p>
    )}
  </div>
  
  <button type="submit">提交</button>
</form>

屏幕阅读器专用内容

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

测试

# 自动化测试
npm install -D axe-core @axe-core/react

# 在测试中
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('组件可访问', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

资源