Communitygithub.com

frontend-a11y

React 和 Next.js 的實用無障礙模式,涵蓋程式碼審查中最常見的問題:遺失的表單標籤、錯誤的 ARIA 用法、非語義化的互動元素,以及鍵盤導航問題。

frontend-a11y 是什麼?

這是專為 React 和 Next.js 開發者設計的無障礙開發技能,旨在解決程式碼審查中最常被標記的問題,包括遺失的表單標籤、錯誤的 ARIA 屬性使用、非語義化的互動元素,以及鍵盤導航中斷等。技能提供了大量可直接複用的 TypeScript/JSX 程式碼片段,涵蓋表單無障礙(標籤連結、必填欄位、錯誤訊息關聯)、語義化 HTML 替代方案、ARIA 屬性的正確使用(如 aria-label、aria-describedby、aria-live、aria-expanded),以及完整鍵盤導航和焦點管理實作(例如自訂下拉選單和 Modal 焦點恢復)。此外,還包含針對圖示、圖片、減少動畫偏好(prefers-reduced-motion)的處理,以及常見反模式的對照檢查表。此技能支援所有使用 React 的開發環境(如 Cursor、Codex、Claude Code),開發者只需在程式碼審查或建構元件時啟用即可直接參考最佳實踐。

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

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

預覽

來自技能 README

frontend-a11y preview
frontend-a11y preview
frontend-a11y preview

說明文件

前端無障礙模式

實用的 React 和 Next.js 無障礙模式。涵蓋程式碼審查中最常被標記的問題:遺失的表單標籤、錯誤的 ARIA 用法、非語義化的互動元素,以及鍵盤導航中斷。

何時啟用

  • 建構或審查表單元件(<input>、<select>、<textarea>)
  • 建立互動元素(模態框、下拉選單、工具提示、分頁標籤)
  • 使用帶有 onClick 的 <div> 或 <span>
  • 為任何元素新增 aria-* 屬性
  • 實作鍵盤導航或焦點管理
  • 收到來自程式碼審查工具(CodeRabbit、ESLint a11y)的無障礙反饋
  • 建構必須支援螢幕閱讀器的元件

表單無障礙

遺失的 htmlFor / id 配對以及斷開的錯誤訊息是程式碼審查中最常見的問題。

標籤連結

// 錯誤:標籤未與輸入框連結 — 螢幕閱讀器無法關聯它們
<label>Email</label>
<input type="email" />

// 正確:htmlFor 與輸入框的 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 = 'Email is required';
    if (!password) newErrors.password = 'Password is required';
    if (Object.keys(newErrors).length) {
      setErrors(newErrors);
      return;
    }
    onSubmit(email, password);
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="email">
          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">
          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">Log in</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 vs aria-labelledby

// aria-label:內聯字串標籤 — 當沒有可見的標籤文字時使用
<button aria-label="Close modal">
  <XIcon />
</button>

// aria-labelledby:參考另一個元素的文字 — 當存在可見標籤時使用
<section aria-labelledby="section-title">
  <h2 id="section-title">Recent Orders</h2>
  {/* 內容 */}
</section>

aria-describedby

// 提供標籤之外的補充描述
<button
  aria-describedby="delete-warning"
  onClick={handleDelete}
> Delete account
</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 狀態改變時,焦點必須邏輯地移動 — 尤其是對於模態框和路由轉換。

Modal 焦點恢復

此範例涵蓋初始焦點和恢復。對於完整的焦點陷阱(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}>Close</button>
    </div>
  );
}

圖片與圖示

// 錯誤:裝飾性圖示被宣告為未標籤的圖片
<img src="/icon.svg" />

// 正確:裝飾性圖片對螢幕閱讀器隱藏
<img src="/decoration.png" alt="" aria-hidden="true" />

// 正確:有意義的圖片帶有描述性替代文字
<img src="/chart.png" alt="月度營業額從一月到三月增長了 23%" />

// 正確:帶有可存取標籤的圖示按鈕
<button aria-label="Delete item">
  <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>

// 錯誤:使用 placeholder 替代 label
<input placeholder="Enter your email" />

// 錯誤:正的 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"
  • 沒有在 <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.

相關技能