mcpField
W
web-accessibility

web-accessibility

github.com/alexanderstephenthompsonデザイン・UI35 回インストール
リポジトリ

web-accessibility について

Web インターフェースの構築やリファクタリングを行う人やエージェント向けに、WCAG 2.1 レベル AA の実装指針を示すスキルです。セマンティック HTML、キーボード操作、ARIA、フォーム、テストを扱い、チェックリストと機械的に検査できるルールも備えています。

主な機能

  1. 01WCAG 2.1 AA 実装ガイド
  2. 02セマンティック HTML と ARIA のパターン
  3. 03キーボード操作とフォーカス管理
  4. 04フォーム、コントラスト、メディアの指針
  5. 05自動・手動テストのチェックリスト

活用シーン

  1. 01新しい Web インターフェースの構築
  2. 02アクセシビリティ上の問題の確認と修正
  3. 03キーボード操作可能なモーダルの設計
  4. 04フォームや動的コンテンツの支援技術対応の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/alexanderstephenthompson/claude-hub --skill web-accessibility

インストール後、Claude Code で/web-accessibilityを入力して読み込まれたか確認します。

詳細

Web アクセシビリティスキル

バージョン: 1.0 標準: WCAG 2.1 レベル AA

アクセシビリティは任意ではありません。これらのパターンにより、すべてのユーザーがアプリケーションを利用できます。

問題点

AI エージェントは、見た目は正しくてもアクセシブルではないインターフェースを構築することがあります。WCAG の基準が明示されていないと、セッションごとに代替テキストが省略され、ボタンの代わりに div が使われ、見出しが誤った階層でネストされ、キーボード操作への対応が抜けてしまいます。意図的にそうしているのではなく、最も手間のかからない方法を選んでしまうためです。セッションごとにアクセシビリティ上の不備が少しずつ異なり、修正が終わりのない作業になります。これらの基準に従えば、最初の要素からアクセシビリティを組み込めます。

利用方法

  • 構築者: HTML またはコンポーネントのマークアップを書く前に ## Builder Checklist を読んでください。アクセシビリティは設計段階で組み込む必要があり、後から ARIA で補うものではありません。
  • リファクタリング担当者: アクセシビリティ違反を見つけるには ## Enforced Rules を使ってください。修正パターンについては説明セクションを読んでください。
  • 両者: 説明セクションが準拠すべき基準です。チェックリストとルール表は、同じ内容を簡潔にまとめたものです。

対象範囲と境界

このスキルの対象:

  • WCAG 2.1 レベル AA への準拠
  • POUR 原則(知覚可能、操作可能、理解可能、堅牢)
  • アクセシビリティのためのセマンティック HTML(ランドマーク、見出し、ボタンとリンクの使い分け)
  • キーボード操作とフォーカス管理
  • モーダルのフォーカストラップ
  • ARIA の使用パターンとロール
  • フォームのアクセシビリティ(ラベル、エラー、必須項目)
  • 色のコントラスト要件
  • 画像やメディアの代替テキスト
  • 動的コンテンツとライブリージョン
  • 動きの低減への対応
  • スクリーンリーダーでのテスト

他のスキルに委ねる内容:

  • design:デザイントークンシステム、全体的なデザイン原則、コンポーネントの状態
  • web-css:CSS の実装詳細、ファイル構成、レスポンシブのブレークポイント

WCAG への準拠、スクリーンリーダーへの対応、フォーカス管理、または ARIA のパターンが必要な場合: このスキルを使用してください。 デザインシステムの原則、トークンの適用、またはレイアウトの考え方が必要な場合: design を使用してください。 CSS アーキテクチャまたはレスポンシブ対応の実装が必要な場合: web-css を使用してください。


基本原則(POUR)

  1. 知覚可能 — ユーザーがすべてのコンテンツを知覚できます(見たり、聞いたり、感じたりできます)。
  2. 操作可能 — ユーザーがすべてのコントロールを操作できます(キーボード、マウス、音声など)。
  3. 理解可能 — ユーザーがコンテンツとインターフェースの動作を理解できます。
  4. 堅牢 — 現在および将来の支援技術でコンテンツが動作します。

まずセマンティック HTML を使う

適切な要素を使う

<header>Site header</header>
<nav>Navigation</nav>
<main>
  <article>
    <h1>Article Title</h1>
    <p>Content...</p>
  </article>
  <aside>Related content</aside>
</main>
<footer>Site footer</footer>


<div class="header">Site header</div>
<div class="nav">Navigation</div>
<div class="main">
  <div class="article">
    <div class="title">Article Title</div>
    <div class="content">Content...</div>
  </div>
</div>
ボタンとリンクの使い分け
// ✅ Button - Performs an action
<button onClick={handleSubmit}>Submit Form</button>
<button onClick={openModal}>Open Settings</button>

// ✅ Link - Navigates somewhere
<a href="/products">View Products</a>
<Link to="/checkout">Proceed to Checkout</Link>

// ❌ Bad - Wrong semantics
<div onClick={handleSubmit}>Submit Form</div>
<a onClick={openModal}>Open Settings</a>  {/* No href! */}
<button onClick={() => navigate('/products')}>View Products</button>
見出しの階層
// ✅ Good - Proper hierarchy
<h1>Page Title</h1>
<section>
  <h2>Section Title</h2>
  <h3>Subsection</h3>
</section>
<section>
  <h2>Another Section</h2>
</section>

// ❌ Bad - Skipped levels
<h1>Page Title</h1>
<h3>Subsection</h3>  {/* Skipped h2! */}
<h5>Deep section</h5> {/* Skipped h4! */}

キーボード操作

フォーカス管理
/* Visible focus indicator */
:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* Remove outline only for mouse users */
:focus:not(:focus-visible) {
  outline: none;
}
タブ順序
// ✅ Good - Logical tab order (follows DOM order)
<form>
  <label htmlFor="email">Email</label>
  <input id="email" type="email" />

  <label htmlFor="password">Password</label>
  <input id="password" type="password" />

  <button type="submit">Login</button>
</form>

// ❌ Bad - Jumpy tab order
<form>
  <button type="submit" tabIndex={1}>Login</button>
  <input tabIndex={3} />
  <input tabIndex={2} />
</form>
スキップリンク
// At the very top of your app
function SkipLink() {
  return (
    <a href="#main-content" className="skip-link">
      Skip to main content
    </a>
  );
}

// CSS
.skip-link {
  position: absolute;
  top: -100%;
  left: var(--space-4);
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface);
  z-index: var(--z-tooltip);
}

.skip-link:focus {
  top: var(--space-4);
}
キーボードショートカット
// Listen for keyboard events
function SearchModal({ isOpen, onClose }) {
  useEffect(() => {
    function handleKeyDown(e) {
      if (e.key === 'Escape') {
        onClose();
      }
    }

    if (isOpen) {
      document.addEventListener('keydown', handleKeyDown);
      return () => document.removeEventListener('keydown', handleKeyDown);
    }
  }, [isOpen, onClose]);

  // ...
}

フォーカストラップ(モーダル)

モーダルのパターン
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

function Modal({ isOpen, onClose, title, children }) {
  const modalRef = useRef(null);
  const previousFocus = useRef(null);

  useEffect(() => {
    if (isOpen) {
      // Store current focus
      previousFocus.current = document.activeElement;

      // Focus the modal
      modalRef.current?.focus();

      // Trap focus inside modal
      const modal = modalRef.current;
      const focusableElements = modal?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      const firstElement = focusableElements?.[0];
      const lastElement = focusableElements?.[focusableElements.length - 1];

      function handleTab(e) {
        if (e.key !== 'Tab') return;

        if (e.shiftKey && document.activeElement === firstElement) {
          e.preventDefault();
          lastElement?.focus();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          e.preventDefault();
          firstElement?.focus();
        }
      }

      modal?.addEventListener('keydown', handleTab);
      return () => modal?.removeEventListener('keydown', handleTab);
    } else {
      // Restore focus when closing
      previousFocus.current?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        className="modal"
        onClick={(e) => e.stopPropagation()}
        tabIndex={-1}
      >
        <h2 id="modal-title">{title}</h2>
        {children}
        <button onClick={onClose} aria-label="Close modal">
          ×
        </button>
      </div>
    </div>,
    document.body
  );
}

ARIA の使用

ARIA を使う場合

ARIA の第一原則: ネイティブ HTML で実現できる場合は、ARIA を使わないでください。

// ❌ Unnecessary ARIA
<div role="button" tabIndex={0} onClick={handleClick}>
  Click me
</div>

// ✅ Just use a button
<button onClick={handleClick}>Click me</button>
必須の ARIA パターン
// Labeling
<button aria-label="Close menu">×</button>
<input aria-labelledby="name-label helper-text" />

// Descriptions
<button aria-describedby="delete-warning">Delete Account</button>
<p id="delete-warning">This action cannot be undone.</p>

// States
<button aria-pressed={isActive}>Toggle</button>
<button aria-expanded={isOpen} aria-controls="menu">Menu</button>
<div id="menu" aria-hidden={!isOpen}>Menu content</div>

// Live regions (for dynamic content)
<div aria-live="polite" aria-atomic="true">
  {statusMessage}
</div>
よく使われる ARIA ロール
ロール使用例
alert重要なメッセージ(エラー、警告)
alertdialogユーザーの応答が必要なモーダル
dialogモーダルダイアログ
navigationナビゲーションセクション
search検索フォーム
tablist、tab、tabpanelタブインターフェース
menu、menuitemドロップダウンメニュー
statusステータスの更新(読み込み中、保存中)

フォーム

ラベル
// ✅ Explicit label association
<label htmlFor="email">Email Address</label>
<input id="email" type="email" />

// ✅ Implicit association (wrapped)
<label>
  Email Address
  <input type="email" />
</label>

// ❌ Bad - No association
<span>Email Address</span>
<input type="email" />
エラーメッセージ
function FormField({ id, label, error, ...props }) {
  const errorId = `${id}-error`;

  return (
    <div className="form-field">
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        aria-invalid={!!error}
        aria-describedby={error ? errorId : undefined}
        {...props}
      />
      {error && (
        <p id={errorId} className="error-message" role="alert">
          {error}
        </p>
      )}
    </div>
  );
}
必須項目
<label htmlFor="name">
  Name <span aria-hidden="true">*</span>
  <span className="visually-hidden">(required)</span>
</label>
<input id="name" required aria-required="true" />

色とコントラスト

最小コントラスト比
テキストの種類比率例
通常のテキスト(< 18px)4.5:1本文
大きなテキスト(≥ 18px または 14px の太字)3:1見出し
UI コンポーネント3:1ボタン、入力欄
色だけに頼らない
// ❌ Bad - Color is only indicator
<span style={{ color: error ? 'red' : 'green' }}>
  {error ? 'Invalid' : 'Valid'}
</span>

// ✅ Good - Color + icon + text
<span className={error ? 'error' : 'success'}>
  {error ? (
    <>
      <ErrorIcon aria-hidden="true" /> Invalid: {error}
    </>
  ) : (
    <>
      <CheckIcon aria-hidden="true" /> Valid
    </>
  )}
</span>

画像とメディア

代替テキスト
// Informative image
<img src="chart.png" alt="Sales increased 40% from January to March" />

// Decorative image
<img src="divider.png" alt="" role="presentation" />

// Complex image
<figure>
  <img src="diagram.png" alt="Architecture diagram" aria-describedby="diagram-desc" />
  <figcaption id="diagram-desc">
    The system consists of three layers: presentation, business logic, and data.
    {/* Full description */}
  </figcaption>
</figure>
SVG アイコン
// Decorative icon (with visible text)
<button>
  <SearchIcon aria-hidden="true" />
  Search
</button>

// Standalone icon (needs label)
<button aria-label="Search">
  <SearchIcon aria-hidden="true" />
</button>

// Icon with title
<svg role="img" aria-labelledby="icon-title">
  <title id="icon-title">Search</title>
  <path d="..." />
</svg>

動的コンテンツ

読み込み状態
function ProductList() {
  const { data, loading, error } = useQuery(GET_PRODUCTS);

  if (loading) {
    return (
      <div role="status" aria-live="polite">
        <Spinner aria-hidden="true" />
        <span className="visually-hidden">Loading products...</span>
      </div>
    );
  }

  if (error) {
    return (
      <div role="alert">
        Error loading products. Please try again.
      </div>
    );
  }

  return (
    <ul aria-label="Products">
      {data.products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}
ライブリージョン
// Polite - Waits for user to finish current task
<div aria-live="polite" aria-atomic="true">
  {saveStatus} {/* "Saving...", "Saved!", etc. */}
</div>

// Assertive - Interrupts immediately (use sparingly)
<div aria-live="assertive" role="alert">
  {errorMessage}
</div>

動きの低減

/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
// In React
function AnimatedComponent() {
  const prefersReducedMotion = window.matchMedia(
    '(prefers-reduced-motion: reduce)'
  ).matches;

  return (
    <motion.div
      animate={{ opacity: 1 }}
      transition={{
        duration: prefersReducedMotion ? 0 : 0.3,
      }}
    />
  );
}

視覚的に隠すテキスト

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
// Use for screen reader only content
<button>
  <TrashIcon aria-hidden="true" />
  <span className="visually-hidden">Delete item</span>
</button>

<a href="/products">
  View all products
  <span className="visually-hidden"> in the catalog</span>
</a>

アクセシビリティのテスト

自動テスト
// jest + jest-axe
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<ProductCard product={mockProduct} />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
手動テストのチェックリスト
  • キーボードのみでページ全体を操作する
  • スクリーンリーダー(VoiceOver、NVDA)を使用する
  • 200% に拡大しても使いやすいか確認する
  • ブラウザーのアクセシビリティインスペクターで確認する
  • ハイコントラストモードでテストする
  • 視差効果を減らす設定を有効にしてテストする

アンチパターン

アンチパターン問題修正方法
あらゆるものにdivを使う支援技術向けのセマンティクスがありませんセマンティックHTMLを使用する
tabIndex > 0自然なタブ順序が崩れます正のtabIndexを削除する
Outline: noneフォーカスインジケーターがありません:focus-visibleを使用する
ARIAの過剰な使用複雑で、エラーが起きやすくなりますまずネイティブHTMLを使用する
色だけで意味を伝える色覚異常のある人には見分けられませんアイコンやテキストを追加する
メディアの自動再生混乱や不快感を招きますユーザーが開始した場合のみ再生する
マウスのみの操作キーボードユーザーを排除しますキーボード操作のイベントハンドラーを追加する
代替テキストがないスクリーンリーダーでは画像が認識できません画像を説明するか、装飾としてマークする
説明のないリンク「ここをクリック」では役に立ちません内容がわかるリンクテキストにする

ビルダー向けチェックリスト

このスキルが適用されるUIコードを書く前に、これらの制約に照らして計画を確認してください。ビルダーはコードを書く前にこのセクションを読み、リファクタリング担当者は「適用ルール」テーブルと全体の説明を使用します。

セマンティックHTML
  • 見出しの階層が正しい
  • 操作にはボタン、ナビゲーションにはリンクを使用する
  • セマンティックなランドマーク(header、nav、main、footer)を使用する
  • リストにはul/ol/liを使用する
キーボード
  • すべてのインタラクティブ要素にフォーカスできる
  • フォーカスインジケーターが見える
  • タブ順序が論理的である
  • スキップリンクがある
  • モーダル内でフォーカスがトラップされる
スクリーンリーダー
  • すべての画像に代替テキストがある
  • フォーム入力にラベルがある
  • エラーメッセージが入力欄に関連付けられている
  • 動的コンテンツにライブリージョンを使用している
  • アイコンにアクセシブルな名前がある
視覚
  • 色のコントラスト比が4.5:1を満たしている
  • 色だけを唯一の指標にしていない
  • 200%の拡大表示で動作する
  • 視差効果を減らす設定が尊重される
フォーム
  • すべての入力欄にラベルがある
  • 必須項目が示されている
  • エラーが明確に識別できる
  • エラーメッセージが役に立つ

適用ルール

これらのルールは、check.js(clean-team)によって決定論的にチェックされます。これらの標準を更新する際は、対応するcheck.jsのルールも更新して内容を一致させてください。逆の場合も同様です。

ルールID重大度チェック内容
img-alt-requirederroralt属性がない<img>
title-requirederror<title>要素がない
tabindex-no-positiveerror正のtabindex値(タブ順序が崩れる)
heading-orderwarn見出しレベルが飛んでいる(h1 → h3)
single-h1warn1ページ内に複数の<h1>要素がある
no-div-as-buttonwarnonclickハンドラーを持つ<div>/<span>

リソース

コメント