Communitygithub.com

前端可访问性模式

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

前端可访问性模式 是什么?

本技能提供了针对 React 和 Next.js 应用的全面前端可访问性指南,重点关注代码评审中反复出现的关键问题:缺失表单标签连接、错误 ARIA 用法、非语义化交互元素以及损坏的键盘导航。文档不仅涵盖了表单可访问性,包括标签关联、必填字段和错误消息的 `aria-describedby` 绑定,还深入讲解了语义 HTML 的正确使用(如使用 `<button>` 而非 `<div>` 实现交互,使用 `<a>` 实现导航)以及 ARIA 属性的正确使用方法(如 `aria-label`、`aria-labelledby`、`aria-describedby` 和 `aria-live`)。技能还提供了完善的键盘导航实现(包括组合框/下拉菜单)和焦点管理(模态框焦点恢复与陷阱)的完整代码示例,讨论了图片和图标的可访问性、尊重用户减少动画偏好(`prefers-reduced-motion`)的实践,并列举了常见的反模式。该技能适用于 Claude Code、Cursor 等 AI 编码助手,可在构建或审查表单组件、创建交互式元素、添加 ARIA 属性或实施键盘导航时激活。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/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>邮箱</label>
<input type="email" />

// 正确:htmlFor 与 input id 匹配
<label htmlFor="email">邮箱</label>
<input id="email" type="email" />

必填字段

// 错误:纯视觉的星号对屏幕阅读器不传达任何信息
<label htmlFor="email">邮箱 *</label>
<input id="email" type="email" />

// 正确:required 启用原生浏览器验证;aria-required 向屏幕阅读器发出信号
<label htmlFor="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}>提交</div>

// 正确:button 可聚焦,在 Enter/Space 上激活,声明为“按钮”
<button type="button" onClick={handleClick}>提交</button>
// 错误:非语义化导航
<div onClick={() => navigate('/home')}>首页</div>

// 正确:anchor 支持右键单击、中键单击和键盘导航
<a href="/home">首页</a>
// 错误:标题层级被跳过(h1 到 h4)
<h1>仪表盘</h1>
<h4>最近活动</h4>

// 正确:顺序标题层级
<h1>仪表盘</h1>
<h2>最近活动</h2>

ARIA 属性

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

aria-label vs 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 这样的库,它可以处理动态内容和嵌套门户等边缘情况。

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}>点击我</div>

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

// 错误:placeholder 用作标签的替代品
<input placeholder="输入您的邮箱" />

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

// 错误:aria-hidden 放在可聚焦元素上——键盘用户会被困住
<button aria-hidden="true">打开</button>

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

检查清单

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

  • 每个 <input>、<select> 和 <textarea> 通过 htmlFor/id 都有一个关联的 <label>
  • 错误消息通过 aria-describedby 链接并标记为 role="alert"
  • <div> 或 <span> 上没有 onClick 而没有 role、tabIndex 和 onKeyDown
  • 仅图标按钮具有 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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能