前端可访问性Skill frontend-accessibility

前端可访问性技能涉及使用语义化HTML、ARIA、键盘导航和屏幕阅读器支持来构建符合WCAG标准的可访问Web应用程序,确保所有用户都能获得包容性的体验。

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

前端可访问性

概览

根据WCAG指南构建可访问的Web应用程序,使用语义化HTML、ARIA属性、键盘导航和屏幕阅读器支持,以实现包容性用户体验。

何时使用

  • 遵守可访问性标准
  • 包容性设计要求
  • 屏幕阅读器支持
  • 键盘导航
  • 颜色对比问题

实施示例

1. 语义HTML和ARIA

<!-- 良好的语义结构 -->
<nav aria-label="主要导航">
  <ul>
    <li><a href="/">首页</a></li>
    <li><a href="/about">关于</a></li>
    <li><a href="/contact">联系</a></li>
  </ul>
</nav>

<main>
  <article>
    <header>
      <h1>文章标题</h1>
      <time datetime="2024-01-15">2024年1月15日</time>
    </header>
    <p>文章内容...</p>
  </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; 2024 公司名称</p>
</footer>

<!-- 带有适当标签的表单 -->
<form>
  <div class="form-group">
    <label for="email">电子邮件地址</label>
    <input
      id="email"
      type="email"
      name="email"
      required
      aria-required="true"
      aria-describedby="email-help"
    />
    <small id="email-help">我们永远不会分享您的电子邮件</small>
  </div>

  <div class="form-group">
    <label for="password">密码</label>
    <input
      id="password"
      type="password"
      name="password"
      required
      aria-required="true"
      aria-describedby="password-requirements"
    />
    <div id="password-requirements">
      <ul>
        <li>至少8个字符</li>
        <li>一个大写字母</li>
        <li>一个数字</li>
      </ul>
    </div>
  </div>

  <button type="submit">注册</button>
</form>

<!-- 带有适当ARIA的模态框 -->
<div
  id="modal"
  role="dialog"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
  aria-modal="true"
>
  <button aria-label="关闭模态框">×</button>
  <h2 id="modal-title">确认操作</h2>
  <p id="modal-description">您确定吗?</p>
  <button>取消</button>
  <button>确认</button>
</div>

<!-- 带有角色的警告 -->
<div role="alert" aria-live="polite">
  <strong>错误:</strong>请更正高亮显示的字段
</div>

2. 键盘导航

// 支持键盘的React组件
import React, { useEffect, useRef, useState } from 'react';

interface MenuItem {
  id: string;
  label: string;
  href: string;
}

const KeyboardNavigationMenu: React.FC<{ items: MenuItem[] }> = ({ items }) => {
  const [activeIndex, setActiveIndex] = useState(0);
  const menuRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      switch (e.key) {
        case 'ArrowLeft':
        case 'ArrowUp':
          e.preventDefault();
          setActiveIndex(prev =>
            prev === 0 ? items.length - 1 : prev - 1
          );
          break;

        case 'ArrowRight':
        case 'ArrowDown':
          e.preventDefault();
          setActiveIndex(prev =>
            prev === items.length - 1 ? 0 : prev + 1
          );
          break;

        case 'Home':
          e.preventDefault();
          setActiveIndex(0);
          break;

        case 'End':
          e.preventDefault();
          setActiveIndex(items.length - 1);
          break;

        case 'Enter':
        case ' ':
          e.preventDefault();
          const link = menuRef.current?.querySelectorAll('a')[activeIndex];
          link?.click();
          break;

        case 'Escape':
          menuRef.current?.querySelector('a')?.blur();
          break;

        default:
          break;
      }
    };

    menuRef.current?.addEventListener('keydown', handleKeyDown);
    return () => menuRef.current?.removeEventListener('keydown', handleKeyDown);
  }, [items.length, activeIndex]);

  return (
    <div role="menubar" ref={menuRef}>
      {items.map((item, index) => (
        <a
          key={item.id}
          href={item.href}
          role="menuitem"
          tabIndex={index === activeIndex ? 0 : -1}
          onFocus={() => setActiveIndex(index)}
          aria-current={index === activeIndex ? 'page' : undefined}
        >
          {item.label}
        </a>
      ))}
    </div>
  );
};

3. 颜色对比度和视觉可访问性

/* 适当的颜色对比度(WCAG AA: 文本4.5:1,大文本3:1) */
:root {
  --color-text: #1a1a1a; /* 黑色 - 高对比度 */
  --color-background: #ffffff;
  --color-primary: #0066cc; /* 蓝色,对比度良好 */
  --color-success: #008000; /* 不是纯绿色 */
  --color-error: #d32f2f; /* 不是纯红色 */
  --color-warning: #ff8c00; /* 不是黄色 */
}

body {
  color: var(--color-text);
  background-color: var(--color-background);
  font-size: 16px;
  line-height: 1.5;
}

a {
  color: var(--color-primary);
  text-decoration: underline; /* 不要仅依赖颜色 */
}

button {
  min-height: 44px; /* 触摸目标大小 */
  min-width: 44px;
  padding: 10px 20px;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
}

/* 键盘导航的焦点可见 */
button:focus-visible,
a:focus-visible,
input:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 2px;
}

/* 高对比度模式支持 */
@media (prefers-contrast: more) {
  body {
    font-weight: 500;
  }

  button {
    border: 2px solid currentColor;
  }
}

/* 减少运动支持 */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* 暗色模式支持 */
@media (prefers-color-scheme: dark) {
  :root {
    --color-text: #e0e0e0;
    --color-background: #1a1a1a;
    --color-primary: #6495ed;
  }
}

4. 屏幕阅读器公告

// 用于公告的LiveRegion组件
interface LiveRegionProps {
  message: string;
  politeness?: 'polite' | 'assertive' | 'off';
  role?: 'status' | 'alert';
}

const LiveRegion: React.FC<LiveRegionProps> = ({
  message,
  politeness = 'polite',
  role = 'status'
}) => {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (message && ref.current) {
      ref.current.textContent = message;
    }
  }, [message]);

  return (
    <div
      ref={ref}
      role={role}
      aria-live={politeness}
      aria-atomic="true"
      className="sr-only"
    />
  );
};

// 组件中的使用
const SearchResults: React.FC = () => {
  const [results, setResults] = useState([]);
  const [message, setMessage] = useState('');

  const handleSearch = async (query: string) => {
    const response = await fetch(`/api/search?q=${query}`);
    const data = await response.json();
    setResults(data);
    setMessage(`找到 ${data.length} 个结果`);
  };

  return (
    <>
      <LiveRegion message={message} />
      <input
        type="text"
        placeholder="搜索..."
        onChange={(e) => handleSearch(e.target.value)}
        aria-label="搜索结果"
      />
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </>
  );
};

// 跳转到主要内容链接(默认隐藏)
const skipLink = document.createElement('a');
skipLink.href = '#main-content';
skipLink.textContent = '跳转到主要内容';
skipLink.style.position = 'absolute';
skipLink.style.top = '-40px';
skipLink.style.left = '0';
skipLink.style.background = '#000';
skipLink.style.color = '#fff';
skipLink.style.padding = '8px';
skipLink.style.zIndex = '100';
skipLink.addEventListener('focus', () => {
  skipLink.style.top = '0';
});
skipLink.addEventListener('blur', () => {
  skipLink.style.top = '-40px';
});
document.body.insertBefore(skipLink, document.body.firstChild);

5. 可访问性测试

// jest-axe集成测试
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from './Button';

expect.extend(toHaveNoViolations);

describe('按钮可访问性', () => {
  it('不应该有可访问性违规', async () => {
    const { container } = render(
      <Button>点击我</Button>
    );

    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it('应该有适当的ARIA标签', async () => {
    const { container } = render(
      <Button aria-label="关闭对话框">×</Button>
    );

    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

// 可访问性检查Hook
const useAccessibilityChecker = () => {
  useEffect(() => {
    // 在开发中运行可访问性检查
    if (process.env.NODE_ENV === 'development') {
      import('axe-core').then(axe => {
        axe.run((error, results) => {
          if (results.violations.length > 0) {
            console.warn('发现可访问性违规:', results.violations);
          }
        });
      });
    }
  }, []);
};

最佳实践

  • 使用语义化HTML元素
  • 为图像提供有意义的alt文本
  • 确保文本颜色对比度比为4.5:1
  • 完全支持键盘导航
  • 仅在必要时使用ARIA
  • 用屏幕阅读器测试(NVDA, JAWS)
  • 实施跳过链接
  • 支持放大至200%
  • 使用描述性链接文本
  • 使用实际的辅助技术进行测试

资源