Accessibility Engineer について
デザインや開発に携わる人向けのアクセシビリティエンジニアリングのスキルガイドです。WCAG 2.1 AA を目標に、セマンティック HTML、キーボード操作、ARIA、フォーム、テストを扱います。
主な機能
- 01セマンティック HTML とページ構造
- 02キーボード操作とフォーカス管理
- 03ARIA の使用指針
- 04フォームのラベルとエラーメッセージ
- 05色のコントラストと画像の代替テキスト
- 06自動・手動のアクセシビリティテスト
活用シーン
- 01ページ全体のキーボード操作を確認
- 02フォームにラベルと関連付けたエラーを追加
- 03コントラストと画像の代替テキストを点検
- 04axe や Lighthouse でアクセシビリティを検査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ daffy0208/ ai-dev-standards --skill accessibility-engineerインストール後、Claude Code で/accessibility-engineerを入力して読み込まれたか確認します。
同梱ファイルREADME.mdmanifest.yaml
詳細
アクセシビリティエンジニア
すべての人のために構築しましょう。アクセシビリティは必須です。
基本原則
アクセシビリティは機能ではなく、市民の権利です。
米国では成人の4人に1人が障害を抱えています。アクセシブルなデザインは、すべての人に役立ちます。
- 視覚障害のあるユーザー(スクリーンリーダー)
- 弱視のユーザー(拡大、高コントラスト)
- 聴覚障害のあるユーザー(字幕)
- 運動障害のあるユーザー(キーボードのみで操作)
- 認知障害のあるユーザー(わかりやすい言葉)
- 一時的な障害のあるユーザー(腕の骨折)
- 状況による制約のあるユーザー(強い日差し、騒がしい環境)
WCAG 適合レベル
レベル A: 最低限(法的要件) レベル AA: 業界標準(これを目指してください) レベル AAA: 最高水準(すべてのコンテンツで達成するのは困難です)
目標:WCAG 2.1 AA への適合
柱 1:セマンティック HTML
適切な要素を使う
// ❌ Bad: Divs for everything (no semantic meaning)
<div onClick={handleClick}>Click me</div>
<div>Menu</div>
// ✅ Good: Semantic HTML
<button onClick={handleClick}>Click me</button>
<nav>Menu</nav>
ドキュメント構造
// ✅ Proper heading hierarchy
<h1>Page Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h2>Section 2</h2>
// ❌ Bad: Skipping levels
<h1>Page Title</h1>
<h4>Section 1</h4> // Skipped h2, h3
ランドマーク
<header>
<nav aria-label="Main navigation">
{/* Navigation links */}
</nav>
</header>
<main>
<article>
{/* Main content */}
</article>
<aside>
{/* Sidebar */}
</aside>
</main>
<footer>
{/* Footer content */}
</footer>
柱 2:キーボード操作
すべてのインタラクティブ要素をキーボードで操作できるようにする
// ✅ Button is keyboard accessible by default
<button onClick={handleClick}>Click me</button>
// ❌ Div requires extra work
<div onClick={handleClick}>Click me</div> // Can't tab to it!
// ✅ If you must use div, add keyboard support
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Click me
</div>
Tab キーの順序
// ✅ Natural tab order (follows DOM order)
<input />
<button>Submit</button>
<a href="/help">Help</a>
// ❌ Don't use tabIndex > 0 (breaks natural order)
<button tabIndex={5}>Button</button> // Anti-pattern!
// ✅ tabIndex=-1 to remove from tab order
<div tabIndex={-1}>Not keyboard focusable</div>
フォーカス管理
// Modal: Trap focus inside
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef()
useEffect(() => {
if (!isOpen) return
// Focus first focusable element
const firstFocusable = modalRef.current.querySelector('button, input, a')
firstFocusable?.focus()
// Trap focus
function handleTab(e) {
if (e.key !== 'Tab') return
const focusableElements = modalRef.current.querySelectorAll(
'button, input, a, [tabindex]:not([tabindex="-1"])'
)
const first = focusableElements[0]
const last = focusableElements[focusableElements.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
last.focus()
e.preventDefault()
}
} else {
if (document.activeElement === last) {
first.focus()
e.preventDefault()
}
}
}
document.addEventListener('keydown', handleTab)
return () => document.removeEventListener('keydown', handleTab)
}, [isOpen])
return isOpen ? (
<div role="dialog" aria-modal="true" ref={modalRef}>
{children}
<button onClick={onClose}>Close</button>
</div>
) : null
}
スキップリンク
// Allow keyboard users to skip navigation
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<nav>{/* Navigation */}</nav>
<main id="main-content">
{/* Main content */}
</main>
// CSS
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
}
.skip-link:focus {
top: 0;
}
柱 3:ARIA 属性
セマンティック HTML だけでは不十分な場合に限り ARIA を使う
// ✅ Semantic HTML (no ARIA needed)
<button>Click me</button>
// ❌ Unnecessary ARIA
<button role="button" aria-label="Click me">Click me</button>
// ✅ ARIA needed (custom widget)
<div role="tab" aria-selected={isActive} aria-controls="panel-1">
Tab 1
</div>
よく使う ARIA 属性
aria-label - アクセシブルな名前を指定します。
<button aria-label="Close dialog">
<XIcon /> {/* Visual only */}
</button>
<input type="search" aria-label="Search products" />
aria-labelledby - 別の要素を参照します。
<h2 id="dialog-title">Delete Account</h2>
<div role="dialog" aria-labelledby="dialog-title">
{/* Dialog content */}
</div>
aria-describedby - 追加の説明を指定します。
<input
type="password"
aria-describedby="password-requirements"
/>
<div id="password-requirements">
Must be at least 8 characters
</div>
aria-live - 動的コンテンツを通知します。
// Polite: Wait for user to finish
<div aria-live="polite">
{itemsAddedToCart} items added to cart
</div>
// Assertive: Interrupt immediately (use sparingly)
<div aria-live="assertive" role="alert">
Error: Payment failed
</div>
aria-expanded - 折りたたみ可能なコンテンツを示します。
<button
aria-expanded={isOpen}
aria-controls="dropdown-menu"
onClick={() => setIsOpen(!isOpen)}
>
Menu
</button>
<div id="dropdown-menu" hidden={!isOpen}>
{/* Menu items */}
</div>
aria-hidden - スクリーンリーダーから隠します。
// Decorative icons
<span aria-hidden="true">★</span>
// Don't hide interactive elements!
// ❌ Bad
<button aria-hidden="true">Click me</button>
柱 4:フォームと入力欄
ラベル
// ✅ Good: Explicit label
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// ✅ Good: Implicit label
<label>
Email
<input type="email" />
</label>
// ❌ Bad: No label (placeholder is not a label!)
<input type="email" placeholder="Email" />
エラーメッセージ
function EmailInput({ error }) {
const errorId = 'email-error'
return (
<>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!error}
aria-describedby={error ? errorId : undefined}
/>
{error && (
<div id={errorId} role="alert">
{error}
</div>
)}
</>
)
}
必須項目
<label htmlFor="name">
Name <span aria-label="required">*</span>
</label>
<input id="name" type="text" required aria-required="true" />
柱 5:色とコントラスト
最低コントラスト比(WCAG AA)
- 通常のテキスト(18pt 未満):4.5:1
- 大きなテキスト(18pt 以上、または 14pt の太字):3:1
- UI コンポーネント:3:1
// ❌ Bad: Insufficient contrast
<button style={{ background: '#ddd', color: '#aaa' }}>
Submit // 1.5:1 contrast - fails!
</button>
// ✅ Good: Sufficient contrast
<button style={{ background: '#0066cc', color: '#ffffff' }}>
Submit // 8:1 contrast - passes!
</button>
色だけに頼らない
// ❌ Bad: Color only
<span style={{ color: 'red' }}>Error</span>
<span style={{ color: 'green' }}>Success</span>
// ✅ Good: Color + icon/text
<span style={{ color: 'red' }}>
<ErrorIcon aria-hidden="true" />
Error
</span>
柱 6:画像とメディア
代替テキスト
// ✅ Informative images
<img src="chart.png" alt="Sales increased 50% in Q4" />
// ✅ Decorative images
<img src="decorative-border.png" alt="" /> // Empty alt
// ❌ Bad: No alt or redundant alt
<img src="photo.jpg" /> // Missing alt
<img src="photo.jpg" alt="Photo" /> // Useless
複雑な画像
<figure>
<img src="complex-chart.png" alt="Sales data for 2024" />
<figcaption>
<details>
<summary>Detailed description</summary>
<p>Q1: $100k, Q2: $150k, Q3: $180k, Q4: $220k. Shows 50% growth year-over-year.</p>
</details>
</figcaption>
</figure>
動画の字幕
<video controls>
<source src="video.mp4" type="video/mp4" />
<track kind="captions" src="captions.vtt" srclang="en" label="English" default />
</video>
柱 7:テスト
自動テスト
# Lighthouse accessibility audit
lighthouse https://example.com --only-categories=accessibility
# axe-core (Jest)
npm install --save-dev @axe-core/react jest-axe
// Test with jest-axe
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('has no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
手動テスト
キーボード操作:
- ページ全体を Tab キーで移動する
- Enter/Space キーでボタンを操作する
- ラジオボタングループでは矢印キーを使う
- Esc キーでモーダルを閉じる
スクリーンリーダーのテスト:
- NVDA(Windows、無料)
- JAWS(Windows、有料)
- VoiceOver(Mac、標準搭載)
スクリーンリーダーのショートカット:
- 見出しで移動:H(次へ)、Shift+H(前へ)
- ランドマークで移動:D(次へ)、Shift+D(前へ)
- すべてのリンクを一覧表示:Insert+F7(NVDA)
よく使うパターン
アクセシブルなボタン
<button
type="button"
onClick={handleClick}
disabled={isDisabled}
aria-busy={isLoading}
aria-label={ariaLabel}
>
{children}
</button>
アクセシブルなモーダル
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Dialog Title</h2>
<p id="dialog-description">Dialog description</p>
<button onClick={onClose}>Close</button>
</div>
アクセシブルなタブ
<div>
<div role="tablist">
<button
role="tab"
aria-selected={activeTab === 'tab1'}
aria-controls="panel1"
onClick={() => setActiveTab('tab1')}
>
Tab 1
</button>
<button
role="tab"
aria-selected={activeTab === 'tab2'}
aria-controls="panel2"
onClick={() => setActiveTab('tab2')}
>
Tab 2
</button>
</div>
<div id="panel1" role="tabpanel" hidden={activeTab !== 'tab1'}>
Panel 1 content
</div>
<div id="panel2" role="tabpanel" hidden={activeTab !== 'tab2'}>
Panel 2 content
</div>
</div>
アクセシビリティチェックリスト
セマンティック HTML
- 適切な見出し階層(h1 → h2 → h3)
- セマンティックなランドマーク(header、nav、main、footer)
- リストには ul/ol/li を使う
- アクションにはボタン、ナビゲーションにはリンクを使う
キーボード
- すべてのインタラクティブ要素をキーボードで操作できる
- フォーカスインジケーターが見える
- Tab キーの順序が論理的である
- スキップリンクがある
- キーボードトラップがない
ARIA
- まずセマンティック HTML を使う(ARIA は必要な場合に限る)
- すべてのインタラクティブウィジェットにロールがある
- 動的コンテンツに aria-live がある
- フォームに適切なラベルと説明がある
色とコントラスト
- テキストのコントラスト比が 4.5:1 以上(通常)、3:1 以上(大きな文字)
- 情報を伝えるために色だけに頼らない
- フォーカスインジケーターが見える
画像とメディア
- すべての画像に代替テキストがある
- 装飾画像の代替テキストが空である
- 動画に字幕がある
- 音声に書き起こしがある
フォーム
- すべての入力欄にラベルがある
- エラーメッセージが入力欄に関連付けられている
- 必須項目が示されている
テスト
- キーボード操作をテストした
- スクリーンリーダーをテストした
- 自動テストツールを実行した(axe、Lighthouse)
- 色覚シミュレーションでテストした
ツール
- axe DevTools - ブラウザー拡張機能
- Lighthouse - Chrome DevTools に組み込み
- WAVE - ウェブアクセシビリティ評価ツール
- Color Contrast Analyzer - デスクトップアプリ
- NVDA - 無料のスクリーンリーダー(Windows)
- VoiceOver - 標準搭載のスクリーンリーダー(Mac)
関連リソース
スキル:
ux-designer- アクセシブルなデザインパターンfrontend-builder- アクセシブルな React コンポーネントtesting-strategist- アクセシビリティテスト
外部リソース:
すべての人のために構築しましょう。 ♿
コメント
Markdown 対応