Communitygithub.com

frontend-a11y

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

frontend-a11y とは?

このスキルは、React と Next.js アプリケーションにおけるフロントエンドアクセシビリティの実践的なパターンを提供します。コードレビューで最も頻繁に指摘される問題(フォームのラベル不足、ARIA の誤った使用法、非セマンティックなインタラクティブ要素、壊れたキーボードナビゲーション)を重点的にカバーしています。具体的なコード例とともに、適切なフォームラベルの接続(htmlFor/id)、必須フィールドの扱い、エラーメッセージの関連付け(aria-describedby)、セマンティックHTMLの使用法、ARIA属性の正しい適用(aria-label, aria-labelledby, aria-live, aria-expanded など)、キーボードナビゲーションの実装、モーダルなどのフォーカス管理、画像とアイコンのアクセシビリティ、そして動きの軽減設定(prefers-reduced-motion)への対応方法を解説します。各セクションには「良い例」と「悪い例」が示され、最終的なチェックリストにより、レビュー前に確認すべき項目が整理されています。このスキルは主に React と Next.js のプロジェクトを対象としており、コードレビューツール(CodeRabbit, ESLint の a11y ルール)からのフィードバックに基づいて活用することが推奨されています。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/frontend-a11y

お気に入りのAIに質問する

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

プレビュー

スキル 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 のペアリングの欠落と、エラーメッセージの接続断は、コードレビューで最も一般的に指摘される問題です。

ラベルの接続

// BAD: ラベルが入力と接続されていない — スクリーンリーダーはそれらを関連付けられない
<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">無効なメールアドレスです</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 = 'メールアドレスは必須です';
    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

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

// BAD: div にはロール、キーボードサポート、アクセシブル名がない
<div onClick={handleClick}>送信</div>

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

// GOOD: a 要素は右クリック、中クリック、キーボードナビゲーションをサポート
<a href="/home">ホーム</a>
// BAD: 見出し階層がスキップされている(h1 から h4)
<h1>ダッシュボード</h1>
<h4>最近のアクティビティ</h4>

// GOOD: 連続した見出しレベル
<h1>ダッシュボード</h1>
<h2>最近のアクティビティ</h2>

ARIA属性

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

画像とアイコン

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

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

// GOOD: 意味のある画像に説明的な代替テキストを付ける
<img src="/chart.png" alt="月間収益が1月から3月にかけて23%増加" />

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

動きの軽減

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

// 使用例
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}>クリックしてください</div>

// BAD: ロールのない div への aria-label
<div aria-label="ナビゲーション">...</div>

// BAD: ラベルの代用としての placeholder
<input placeholder="メールアドレスを入力" />

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

// BAD: フォーカス可能な要素への aria-hidden — キーボードユーザーがトラップされる
<button aria-hidden="true">開く</button>

// BAD: キーボードハンドラのない 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/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.

関連スキル