Communitygithub.com

フロントエンドアクセシビリティパターン

React と Next.js 向けの実践的なアクセシビリティパターン。コードレビューでよく指摘される問題(フォームラベルの欠落、ARIA の誤用、セマンティックでないインタラクティブ要素、キーボードナビゲーションの破綻)をカバーします。

フロントエンドアクセシビリティパターン とは?

このスキルは、React および Next.js アプリケーションにおける、実践的かつコードレビューで頻繁に指摘されるアクセシビリティ問題に対処するためのパターンを提供します。具体的には、フォームラベルと入力フィールドの正しい関連付け、必須フィールドの適切なマークアップ、エラーメッセージと入力要素の紐付け、セマンティック HTML の使用、適切な ARIA 属性の活用(`aria-label`、`aria-labelledby`、`aria-describedby`、`aria-live`、`aria-expanded` など)、キーボードナビゲーションの実装(カスタムドロップダウン)、フォーカス管理(モーダルのフォーカス保存と復元)、画像とアイコンのアクセシビリティ、`prefers-reduced-motion` への対応、そしてアンチパターンの回避方法を網羅しています。コード例は TypeScript と React Hooks を用いて具体的に示されており、開発者はこのスキルを参考にすることで、スクリーンリーダーやキーボードのみのユーザーを含むすべてのユーザーにとって使いやすいコンポーネントを構築できます。主に React、Next.js プロジェクトを対象とし、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に質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

プレビュー

スキル README より

フロントエンドアクセシビリティパターン preview
フロントエンドアクセシビリティパターン preview
フロントエンドアクセシビリティパターン preview

ドキュメント

フロントエンドアクセシビリティパターン は何をしますか?

React と Next.js 向けの実践的なアクセシビリティパターン。コードレビューで最も頻繁に指摘される問題(フォームラベルの欠落、ARIA の誤用、セマンティックでないインタラクティブ要素、キーボードナビゲーションの破綻)をカバーします。

アクティブにするタイミング

  • フォームコンポーネント(<input>、<select>、<textarea>)の構築またはレビュー時
  • インタラクティブ要素(モーダル、ドロップダウン、ツールチップ、タブ)の作成時
  • onClick を持つ <div> または <span> の使用時
  • 任意の要素への aria-* 属性の追加時
  • キーボードナビゲーションまたはフォーカス管理の実装時
  • コードレビューツール(CodeRabbit、ESLint a11y)からのアクセシビリティフィードバック受領時
  • スクリーンリーダーをサポートする必要があるコンポーネントの構築時

フォームアクセシビリティ

htmlFor / id のペアリングの欠落や、エラーメッセージの未接続は、コードレビューで最も頻繁に指摘される問題です。

ラベルの接続

// BAD: label が input に接続されていない — スクリーンリーダーはそれらを関連付けられない
<label>Email</label>
<input type="email" />

// GOOD: htmlFor が input の id と一致している
<label htmlFor="email">Email</label>
<input id="email" type="email" />

必須フィールド

// BAD: 視覚的なアスタリスクのみではスクリーンリーダーに何も伝えない
<label htmlFor="email">Email *</label>
<input id="email" type="email" />

// GOOD: required によりネイティブブラウザ検証が有効に; aria-required でスクリーンリーダーに通知
<label htmlFor="email">
  Email <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />

エラーメッセージ

// BAD: エラーテキストは視覚的に存在するが、入力フィールドにリンクされていない
<input id="email" type="email" />
<span className="error">Invalid email address</span>

// GOOD: 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

意図に合った要素を使用してください。スクリーンリーダーとキーボードユーザーはネイティブのセマンティクスに依存しています。

// BAD: div には role がなく、キーボードサポートもアクセシブルな名前もない
<div onClick={handleClick}>Submit</div>

// GOOD: button はフォーカス可能で、Enter/Space でアクティブになり、「ボタン」としてアナウンスされる
<button type="button" onClick={handleClick}>Submit</button>
// BAD: セマンティックでないナビゲーション
<div onClick={() => navigate('/home')}>Home</div>

// GOOD: anchor は右クリック、中クリック、キーボードナビゲーションをサポート
<a href="/home">Home</a>
// BAD: 見出し階層がスキップされている (h1 から h4)
<h1>Dashboard</h1>
<h4>Recent Activity</h4>

// GOOD: 連続した見出しレベル
<h1>Dashboard</h1>
<h2>Recent Activity</h2>

ARIA 属性

ARIA はネイティブ HTML のセマンティクスが不十分な場合にのみ使用してください。間違った 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>
  {/* content */}
</section>

aria-describedby

// ラベルを超えた補足説明を提供
<button
  aria-describedby="delete-warning"
  onClick={handleDelete}
> Delete account
</button>
<p id="delete-warning">This action cannot be undone.</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}>Close</button>
    </div>
  );
}

画像とアイコン

// BAD: 装飾アイコンがラベルのない画像としてアナウンスされる
<img src="/icon.svg" />

// GOOD: 装飾画像をスクリーンリーダーから隠す
<img src="/decoration.png" alt="" aria-hidden="true" />

// GOOD: 説明的な alt テキストを持つ意味のある画像
<img src="/chart.png" alt="Monthly revenue increased 23% from January to March" />

// GOOD: アクセシブルなラベルを持つアイコンボタン
<button aria-label="Delete item">
  <TrashIcon aria-hidden="true" />
</button>

動作の軽減 (Reduced Motion)

OS 設定で動作の軽減をリクエストしているユーザーを尊重します。

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;
}

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

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

アンチパターン

// BAD: インタラクティブでない要素に onClick、キーボードサポートなし
<div onClick={handleClick}>Click me</div>

// BAD: role のない div に aria-label
<div aria-label="Navigation">...</div>

// BAD: placeholder をラベルの代わりとして使用
<input placeholder="Enter your email" />

// BAD: 正の tabIndex は予測不可能なタブ順序を作成
<button tabIndex={3}>Submit</button>

// BAD: フォーカス可能な要素に aria-hidden — キーボードユーザーが閉じ込められる
<button aria-hidden="true">Open</button>

// BAD: キーボードハンドラなしの role="button" on div
<div role="button" onClick={handleClick}>Submit</div>
// 欠落: tabIndex={0}, onKeyDown (Enter/Space 用)

チェックリスト

インタラクティブコンポーネントをレビューに提出する前に:

  • すべての <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.

関連スキル