Communitygithub.com

前端无障碍模式

React 和 Next.js 中的实用无障碍模式。涵盖了代码审查中最常见的问题:缺失的表单标签、错误的 ARIA 用法、非语义化的交互式元素以及损坏的键盘导航。

前端无障碍模式 是什么?

这是一个专注于 React 和 Next.js 应用的前端无障碍(a11y)技能。它提供了经过实战检验的模式,用于处理代码审查中最常被标记的无障碍问题,包括缺失的 `htmlFor`/`id` 标签关联、表单中的错误消息连接、非语义化元素的使用、不当的 ARIA 属性应用,以及键盘导航和焦点管理。该技能提供了可直接复用的 TypeScript/JSX 代码示例,涵盖了表单、自定义下拉框、模态框和手风琴等常见组件。此外,它还强调了使用 `aria-live` 进行屏幕阅读器通知、链接 `aria-describedby` 的错误消息、根据 `prefers-reduced-motion` 进行动画适配,并包含了用于代码审查的清单。该文档针对所有 React 项目,支持代码审查工具(如 CodeRabbit、ESLint a11y)的审查建议,以及手动无障碍测试。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/frontend-a11y

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

预览

来自技能 README

前端无障碍模式 preview
前端无障碍模式 preview
前端无障碍模式 preview

文档

前端无障碍模式 是做什么的?

适用于 React 和 Next.js 的实用无障碍模式。涵盖了代码审查中最常标记的问题:缺失的表单标签、错误的 ARIA 用法、非语义化的交互式元素以及损坏的键盘导航。

何时激活

  • 构建或审查表单组件(<input>、<select>、<textarea>)
  • 创建交互式元素(模态框、下拉菜单、工具提示、标签页)
  • 使用绑定 onClick 的 <div> 或 <span>
  • 为任何元素添加 aria-* 属性
  • 实现键盘导航或焦点管理
  • 收到来自代码审查工具(CodeRabbit、ESLint a11y)的无障碍反馈
  • 构建需要支持屏幕阅读器的组件

表单无障碍

缺失 htmlFor / id 配对以及断开的错误消息连接是代码审查中最常标记的问题。

标签连接

// 错误:标签与输入框没有连接——屏幕阅读器无法将它们关联起来
<label>Email</label>
<input type="email" />

// 正确:htmlFor 匹配 input 的 id
<label htmlFor="email">Email</label>
<input id="email" type="email" />

必填字段

// 错误:仅视觉上的星号对屏幕阅读器没有任何含义
<label htmlFor="email">Email *</label>
<input id="email" type="email" />

// 正确:required 启用原生浏览器验证;aria-required 向屏幕阅读器发出信号
<label htmlFor="email">
  Email <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />

错误消息

// 错误:错误文本在视觉上存在,但未链接到输入框
<input id="email" type="email" />
<span className="error">无效的电子邮件地址</span>

// 正确:aria-describedby 将输入框连接到其错误消息
// aria-invalid 向屏幕阅读器发出无效状态信号
<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid={!!error}
/>
{error && (
  <span id="email-error" role="alert">
    {error}
  </span>
)}

完整的无障碍表单

interface LoginFormProps {
  onSubmit: (email: string, password: string) => void;
}

export function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState<{ email?: string; password?: string }>({});

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const newErrors: typeof errors = {};
    if (!email) newErrors.email = '需要填写电子邮件';
    if (!password) newErrors.password = '需要填写密码';
    if (Object.keys(newErrors).length) {
      setErrors(newErrors);
      return;
    }
    onSubmit(email, password);
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="email">
          电子邮件 <span aria-hidden="true">*</span>
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          aria-required="true"
          aria-describedby={errors.email ? 'email-error' : undefined}
          aria-invalid={!!errors.email}
          autoComplete="email"
        />
        {errors.email && (
          <span id="email-error" role="alert">
            {errors.email}
          </span>
        )}
      </div>

      <div>
        <label htmlFor="password">
          密码 <span aria-hidden="true">*</span>
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={e => setPassword(e.target.value)}
          aria-required="true"
          aria-describedby={errors.password ? 'password-error' : undefined}
          aria-invalid={!!errors.password}
          autoComplete="current-password"
        />
        {errors.password && (
          <span id="password-error" role="alert">
            {errors.password}
          </span>
        )}
      </div>

      <button type="submit">登录</button>
    </form>
  );
}

语义化 HTML

使用与意图匹配的元素。屏幕阅读器和键盘用户依赖原生语义。

// 错误:div 没有角色、没有键盘支持、没有可访问的名称
<div onClick={handleClick}>Submit</div>

// 正确:button 可获得焦点,在 Enter/Space 上激活,宣布为“按钮”
<button type="button" onClick={handleClick}>Submit</button>
// 错误:非语义化导航
<div onClick={() => navigate('/home')}>Home</div>

// 正确:锚点支持右键单击、中键单击和键盘导航
<a href="/home">Home</a>
// 错误:标题层级被跳过(h1 到 h4)
<h1>Dashboard</h1>
<h4>Recent Activity</h4>

// 正确:顺序的标题层级
<h1>Dashboard</h1>
<h2>Recent Activity</h2>

ARIA 属性

仅当原生 HTML 语义不足时才使用 ARIA。错误的 ARIA 比没有 ARIA 更糟糕。

aria-label 与 aria-labelledby

// aria-label:内联字符串标签——当没有可见的标签文本时使用
<button aria-label="关闭模态框">
  <XIcon />
</button>

// aria-labelledby:引用另一个元素的文本——当存在可见标签时使用
<section aria-labelledby="section-title">
  <h2 id="section-title">最近订单</h2>
  {/* 内容 */}
</section>

aria-describedby

// 提供超出标签的补充描述
<button
  aria-describedby="delete-warning"
  onClick={handleDelete}
> 删除账户
</button>
<p id="delete-warning">此操作无法撤消。</p>

动态内容的 aria-live

// 使用 aria-live 宣布无需重新加载页面即可更新的内容
// polite:等待用户完成当前操作后再宣布
// assertive:立即中断——仅用于紧急错误

export function StatusMessage({ message, isError }: { message: string; isError?: boolean }) {
  return (
    <div role="status" aria-live={isError ? 'assertive' : 'polite'} aria-atomic="true">
      {message}
    </div>
  );
}

aria-expanded 和 aria-controls

export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);
  const contentId = useId();

  return (
    <div>
      <button aria-expanded={isOpen} aria-controls={contentId} onClick={() => setIsOpen(prev => !prev)}>
        {title}
      </button>
      <div id={contentId} hidden={!isOpen}>
        {children}
      </div>
    </div>
  );
}

键盘导航

每个交互式元素都必须能够单独通过键盘到达和操作。

自定义下拉框

export function Dropdown({ options, onSelect }: { options: string[]; onSelect: (value: string) => void }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(0);
  const listId = useId();

  if (!options.length) return null;

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(i => Math.min(i + 1, options.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(i => Math.max(i - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (isOpen) onSelect(options[activeIndex]);
        setIsOpen(prev => !prev);
        break;
      case 'Escape':
        setIsOpen(false);
        break;
    }
  };

  return (
    <div
      role="combobox"
      aria-expanded={isOpen}
      aria-haspopup="listbox"
      aria-controls={listId}
      tabIndex={0}
      onKeyDown={handleKeyDown}
      onClick={() => setIsOpen(prev => !prev)}
    >
      <span>{options[activeIndex]}</span>
      {isOpen && (
        <ul id={listId} role="listbox">
          {options.map((option, index) => (
            <li
              key={option}
              role="option"
              aria-selected={index === activeIndex}
              onClick={() => {
                onSelect(option);
                setIsOpen(false);
              }}
            >
              {option}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

焦点管理

当 UI 状态发生变化时,焦点必须逻辑地移动——尤其是对于模态框和路由转换。

模态框焦点恢复

此示例涵盖了初始焦点和恢复。对于完整的焦点陷阱(在模态框内循环 Tab/Shift+Tab),请使用像 focus-trap-react 这样的库,它可以处理动态内容和嵌套 portal 等边缘情况。

export function Modal({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      // 保存当前聚焦的元素并将焦点移入模态框
      previousFocusRef.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      // 将焦点恢复到打开模态框的元素上
      previousFocusRef.current?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div ref={modalRef} role="dialog" aria-modal="true" aria-labelledby="modal-title" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()}>
      <h2 id="modal-title">{title}</h2>
      {children}
      <button onClick={onClose}>关闭</button>
    </div>
  );
}

图片和图标

// 错误:装饰性图标被宣布为无标签的图像
<img src="/icon.svg" />

// 正确:对屏幕阅读器隐藏装饰性图像
<img src="/decoration.png" alt="" aria-hidden="true" />

// 正确:带有描述性 alt 文本的有意义图像
<img src="/chart.png" alt="月收入从一月到三月增长了 23%" />

// 正确:带有可访问标签的图标按钮
<button aria-label="删除项目">
  <TrashIcon aria-hidden="true" />
</button>

减少动画

尊重那些在操作系统设置中请求了减少动画的用户。

export function useReducedMotion(): boolean {
  const [prefersReduced, setPrefersReduced] = useState(false);

  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    setPrefersReduced(mq.matches);
    const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, []);

  return prefersReduced;
}

// 用法
export function AnimatedCard({ children }: { children: React.ReactNode }) {
  const reduceMotion = useReducedMotion();

  return (
    <div
      style={{
        transition: reduceMotion ? 'none' : 'transform 300ms ease'
      }}
    >
      {children}
    </div>
  );
}

反模式

// 错误:onClick 放在无键盘支持的非交互式元素上
<div onClick={handleClick}>Click me</div>

// 错误:在没有任何角色的 div 上使用 aria-label
<div aria-label="Navigation">...</div>

// 错误:占位符用作标签的替代品
<input placeholder="请输入您的电子邮件" />

// 错误:正数 tabIndex 创建不可预测的 Tab 顺序
<button tabIndex={3}>Submit</button>

// 错误:在可获得焦点的元素上使用 aria-hidden——键盘用户将被困住
<button aria-hidden="true">Open</button>

// 错误:在没有键盘处理程序的 div 上使用 role="button"
<div role="button" onClick={handleClick}>Submit</div>
// 缺失:tabIndex={0},用于处理 Enter/Space 的 onKeyDown

清单

在提交任何交互式组件供审查之前:

  • 每个 <input>、<select> 和 <textarea> 都通过 htmlFor/id 连接了 <label>
  • 错误消息通过 aria-describedby 连接并标记了 role="alert"
  • 在没有 role、tabIndex 和 onKeyDown 的情况下,没有在 <div> 或 <span> 上使用 onClick
  • 仅图标的按钮具有 aria-label
  • 装饰性图像使用 alt="" 和 aria-hidden="true"
  • 模态框在关闭时恢复焦点(如需通过 Tab/Shift+Tab 循环实现完整的焦点陷阱,请使用像 focus-trap-react 这样的库)
  • 动态内容更新使用 aria-live
  • 动画需要尊重 prefers-reduced-motion

相关技能

  • frontend-patterns — 通用 React 组件和状态模式
  • design-system — 设计令牌和组件一致性
  • motion-foundations 和 motion-patterns:带有无障碍考量的动画模式

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相关技能