web-accessibility について
Web開発者向けのアクセシビリティスキルです。セマンティックHTML、キーボード操作、ARIA、フォーム、タッチ操作、国際化の実装とレビューを案内します。体系的な確認に使えるチェックリストも含みます。
主な機能
- 01セマンティックなHTML要素の選択
- 02キーボード操作と可視フォーカスへの対応
- 03動的コンテンツへのARIAの使用
- 04フォームのラベルとエラーの関連付け
- 05タッチターゲットとロケール形式の確認
- 06チェックリストによる実装レビュー
活用シーン
- 01キーボードで操作できるインターフェースの設計
- 02フォームのラベルとエラーメッセージの確認
- 03動的な通知をスクリーンリーダーに伝達
- 04モバイルのタッチ領域とフォーカス表示の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ s-hiraoku/ synapse-a2a --skill web-accessibilityインストール後、Claude Code で/web-accessibilityを入力して読み込まれたか確認します。
詳細
ウェブアクセシビリティ
誰もが利用できるインターフェースを構築しましょう。これらは任意の追加機能ではなく、品質の基本です。
セマンティック HTML
用途に合った要素を使いましょう。インタラクティブ要素を<div>で模倣してはいけません。
// BAD: div with click handler
<div onClick={handleClick} className="button">Submit</div>
// GOOD: semantic button
<button onClick={handleClick}>Submit</button>
// BAD: div as link
<div onClick={() => router.push('/about')}>About</div>
// GOOD: anchor/Link for navigation
<Link href="/about">About</Link>
要素の選択ガイド
| 目的 | 要素 | 使用しない要素 |
|---|---|---|
| アクション(送信、切り替え、削除) | <button> | <div onClick> |
| URL へのナビゲーション | <a> / <Link> | <button onClick={navigate}> |
| フォーム入力 | <input>、<select>、<textarea> | <div>ベースのカスタム入力 |
| セクション見出し | <h1>~<h6>(順番に使用) | <div className="heading"> |
| 項目のリスト | <ul> / <ol> + <li> | 繰り返し使用する<div> |
| ナビゲーショングループ | <nav> | <div className="nav"> |
| メインコンテンツ | <main> | <div id="content"> |
キーボードナビゲーション
すべてのインタラクティブ要素はキーボードで操作できる必要があります。
フォーカス管理
/* NEVER remove focus indicators without replacement */
/* BAD */
*:focus { outline: none; }
/* GOOD: Visible focus only on keyboard navigation */
.interactive:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Group focus for compound controls */
.input-group:focus-within {
outline: 2px solid var(--color-accent);
}
キーボードイベントの処理
// Interactive custom elements need keyboard support
function CustomButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
{children}
</div>
);
}
// Better: just use <button> and avoid all of the above
スキップリンク
キーボードユーザー向けにスキップナビゲーションを用意しましょう。
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50">
Skip to main content
</a>
スクロール先として使用する見出しには、固定ヘッダーを考慮したオフセットが必要です。
[id] { scroll-margin-top: 5rem; }
ARIA パターン
アイコンボタン
// Icon-only buttons MUST have aria-label
<button aria-label="Close dialog" onClick={onClose}>
<XIcon aria-hidden="true" />
</button>
// Decorative icons are hidden from screen readers
<span aria-hidden="true">🔒</span> Secure connection
ライブリージョン
動的なコンテンツの変更をスクリーンリーダーに通知します。
// Toast notifications
<div role="status" aria-live="polite">
{notification && <p>{notification.message}</p>}
</div>
// Error alerts
<div role="alert" aria-live="assertive">
{error && <p>{error.message}</p>}
</div>
読み込み状態
<button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? 'Saving\u2026' : 'Save'} {/* proper ellipsis character */}
</button>
// Skeleton screens
<div aria-busy="true" aria-label="Loading content">
<Skeleton />
</div>
フォーム
ラベル
すべての入力には関連付けられたラベルが必要です。
// GOOD: Explicit association
<label htmlFor="email">Email</label>
<input id="email" type="email" autoComplete="email" />
// GOOD: Wrapping (clickable label, no htmlFor needed)
<label>
Email
<input type="email" autoComplete="email" />
</label>
// GOOD: Visually hidden but accessible
<label htmlFor="search" className="sr-only">Search</label>
<input id="search" type="search" placeholder="Search..." />
入力タイプとオートコンプリート
適切なモバイルキーボードとブラウザーの動作を利用するため、セマンティックな入力タイプを使いましょう。
<input type="email" autoComplete="email" />
<input type="tel" autoComplete="tel" />
<input type="url" autoComplete="url" />
<input type="password" autoComplete="current-password" />
<input type="password" autoComplete="new-password" />
検証とエラー
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" role="alert" className="text-red-600">
{errors.email}
</p>
)}
</div>
フォームの動作ルール
- すべての入力でペーストを禁止しない
- メールアドレスとコードではスペルチェックを無効にする:
spellCheck={false} - リクエストが開始するまで送信ボタンを有効にしておく。読み込み中はスピナーを表示する
- 送信失敗時は最初のエラーにフォーカスする
- チェックボックスとラジオボタン:ラベルと入力の間に操作できない領域を作らず、クリック対象を一体化する
画像とメディア
// Informative images: descriptive alt
<img src="chart.png" alt="Revenue grew 40% from Q1 to Q3 2025" />
// Decorative images: empty alt
<img src="divider.svg" alt="" />
// Prevent layout shift: always set dimensions
<img src="photo.jpg" width={800} height={600} alt="Team photo" />
// Below fold: lazy load
<img src="photo.jpg" loading="lazy" alt="..." />
// Critical: prioritize
<img src="hero.jpg" fetchPriority="high" alt="..." />
タッチ操作とモバイル
タッチターゲット
すべてのインタラクティブ要素は最小44x44pxにします(WCAG 2.5.5)。
.touch-target {
min-height: 44px;
min-width: 44px;
}
セーフエリア
デバイスのノッチとホームインジケーターに対応します。
.full-bleed {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
タッチ操作
/* Prevent 300ms delay and highlight flash */
.interactive {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Prevent scroll chaining on modals */
.modal { overscroll-behavior: contain; }
国際化
// BAD: Hardcoded formats
const date = `${month}/${day}/${year}`;
const price = `$${amount.toFixed(2)}`;
// GOOD: Locale-aware formatting
const date = new Intl.DateTimeFormat(locale).format(new Date());
const price = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD',
}).format(amount);
// Detect language
const lang = request.headers.get('accept-language')?.split(',')[0] ?? 'en';
アクセシビリティのためのパフォーマンス
- 50項目を超えるリストは仮想化する
- 重要なフォントには
font-display: swapを指定した<link rel="preload" as="font">を使用する - レンダリング中のレイアウト読み取りを避ける(スクリーンリーダーユーザーにとっても動作がカクつく原因になります)
- 大規模なフォームでは、非制御入力のほうが制御入力よりもパフォーマンスに優れます
チェックリスト
レビュー時に使用してください。
- すべてのインタラクティブ要素をキーボードで操作できる
-
:focus-visibleでフォーカスインジケーターが表示される - 色のコントラストがWCAG AAを満たしている(本文は4.5:1、大きな文字は3:1)
- 画像に適切な代替テキストがある
- フォーム入力にラベルがある
- エラーメッセージが
aria-describedbyで入力に関連付けられている - アイコンのみのボタンに
aria-labelがある - 動的コンテンツに
aria-liveリージョンを使用している - タッチターゲットが最小44x44pxである
-
prefers-reduced-motionに対応している - 代替のフォーカススタイルなしに
outline: noneを使用していない - 見出し階層が順番どおりである(レベルを飛ばしていない)
コメント
Markdown 対応