Communitygithub.com

前端無障礙模式

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

前端無障礙模式 是什麼?

這個技能提供了一個全面的指南,用於在 React 和 Next.js 應用程式中實作網頁無障礙性。它專注於解決程式碼審查中最常被標記的實際問題,例如遺失的表單標籤、錯誤的 ARIA 屬性使用、非語意的互動元素以及損壞的鍵盤導航。該技能包含可立即應用的 TypeScript 程式碼範例,涵蓋表單無障礙性(標籤連接、必填欄位、錯誤訊息)、語意化 HTML、正確的 ARIA 屬性使用(包括 aria-label、aria-labelledby、aria-describedby、aria-live 和 aria-expanded)、鍵盤導航模式(例如自訂下拉選單)、焦點管理(包括模態視窗的焦點恢復)、圖片和圖示的最佳實踐、尊重使用者減少動畫偏好設定,以及一個常見反模式的清單。支援所有使用 React 的網頁開發平台,特別是 Next.js。它透過提供清晰的「好」與「壞」的程式碼對比和一個最終的檢查清單來運作,幫助開發者在提交前審查其互動式元件。

相容平台~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}>提交</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" />

// 好:有意義的圖片附帶描述性的替代文字
<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 替代 label
<input placeholder="請輸入您的電子郵件" />

// 不好:正數的 tabIndex 建立了不可預測的頁籤順序
<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"
  • 沒有在沒有 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.

相關技能