フレームワーク固有のアクセシビリティパターン
React / Next.js
よくある落とし穴
| パターン | 問題 | 修正方法 |
|---|
<div> の onClick | キーボードで操作できません | <button> を使うか、role="button"、tabIndex={0}、onKeyDown を追加します |
dangerouslySetInnerHTML | アクセシビリティに配慮されていないコンテンツが挿入される可能性があります | 挿入する HTML の ARIA、見出し、代替テキストを監査します |
ルートとしての React.Fragment | ランドマークツリーが壊れる可能性があります | フラグメントがランドマークのネストを妨げないようにします |
リストの key がない | 再レンダリング時にフォーカスが失われる可能性があります | インタラクティブなリストでは安定したキー(配列のインデックス以外)を使います |
| フォーカストラップのないポータル | フォーカスが背景に移動する可能性があります | ポータルのコンテンツを FocusTrap コンポーネントで囲みます |
フォーカス管理に useEffect を使用 | マウント時にフォーカスが設定されない可能性があります | 適切な依存配列を指定して、useRef と useEffect を使います |
修正テンプレート
// Bad: div as button
<div onClick={handleClick}>Submit</div>
// Good: semantic button
<button onClick={handleClick}>Submit</button>
// Bad: image without alt in Next.js
<Image src="/hero.jpg" width={800} height={400} />
// Good: image with alt
<Image src="/hero.jpg" width={800} height={400} alt="Team collaborating in a modern office" />
// Bad: no focus management on route change
useEffect(() => {
// nothing
}, [location]);
// Good: focus management on route change
useEffect(() => {
const mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.focus();
mainContent.scrollIntoView();
}
}, [location]);
// Bad: link opening new tab
<a href={url} target="_blank">Resource</a>
// Good: link with new tab warning
<a href={url} target="_blank" rel="noopener noreferrer">
Resource <span className="sr-only">(opens in new tab)</span>
</a>
Vue
よくある落とし穴
| パターン | 問題 | 修正方法 |
|---|
ユーザーコンテンツを含む v-html | アクセシビリティに配慮されていないマークアップが挿入される可能性があります | 挿入する HTML をサニタイズして監査します |
ライブリージョンでの v-if | 要素が DOM から削除され、通知が機能しなくなります | ライブリージョンでは代わりに v-show を使います |
フォーカス管理のない <transition> | コンテンツの切り替え時にフォーカスが失われます | @after-enter フックでフォーカスを管理します |
<body> への <teleport> | コンテンツがアプリのランドマークツリーの外側に配置されます | テレポートしたコンテンツにランドマークのロールを追加します |
修正テンプレート
<div v-if="message" aria-live="polite">{{ message }}</div>
<div v-show="message" aria-live="polite">{{ message }}</div>
<transition name="fade">
<div v-if="showModal" class="modal">...</div>
</transition>
<transition name="fade" @after-enter="focusModal">
<div v-if="showModal" ref="modal" class="modal" tabindex="-1">...</div>
</transition>
Angular
よくある落とし穴
| パターン | 問題 | 修正方法 |
|---|
[aria-label] バインディング | 無効です。ARIA はプロパティではありません | [attr.aria-label] を使います |
trackBy のない *ngFor | リストの再レンダリング時にフォーカスが失われます | trackBy 関数を追加します |
LiveAnnouncer がない | ルートの変更が通知されません | LiveAnnouncer をインジェクトして、ナビゲーションを通知します |
OnPush とライブリージョンの併用 | 変更検知が実行されない可能性があります | ChangeDetectorRef.markForCheck() を使います |
修正テンプレート
// Bad: ARIA binding
<button [aria-label]="label">X</button>
// Good: ARIA attribute binding
<button [attr.aria-label]="label">X</button>
// Bad: ngFor without trackBy
<li *ngFor="let item of items">{{ item.name }}</li>
// Good: ngFor with trackBy
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
// Route change announcements
constructor(private liveAnnouncer: LiveAnnouncer, private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.liveAnnouncer.announce(`Navigated to ${this.getPageTitle()}`);
});
}
Svelte
よくある落とし穴
| パターン | 問題 | 修正方法 |
|---|
フォーカス管理のない {#if} | コンテンツが表示されたときにフォーカスが失われます | use:action を使って新しいコンテンツにフォーカスします |
モーション設定の確認がない transition: | ユーザーの設定に関係なくアニメーションが再生されます | prefers-reduced-motion の確認を追加します |
インタラクティブでない要素の on:click | キーボードで操作できません | <button> を使うか、キーボードイベントハンドラーを追加します |
修正テンプレート
<div on:click={toggle}>Toggle</div>
<button on:click={toggle}>Toggle</button>
<div transition:fly={{ y: 200 }}>Content</div>
<div transition:fly={{ y: reducedMotion ? 0 : 200, duration: reducedMotion ? 0 : 300 }}>Content</div>
<script>
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
</script>
Tailwind CSS
よくある落とし穴
| パターン | 問題 | 修正方法 |
|---|
outline-none | フォーカスインジケーターが削除されます | ring-2 ring-offset-2 focus-visible:ring-blue-500 と組み合わせます |
bg-white 上の text-gray-400 | コントラスト比が 4.5:1 を下回ります | text-gray-600 またはそれより濃い色を使います |
motion-reduce: バリアントがない | アニメーションがユーザーの設定を無視します | motion-reduce:transition-none を追加します |
focus: スタイルがない | 視認できるフォーカスインジケーターがありません | focus:ring-2 focus:ring-blue-500 を追加します |
sr-only がない | スクリーンリーダー向けのテキストが利用できません | <span class="sr-only">description</span> を追加します |
コントラストに安全な Tailwind の組み合わせ
| 背景 | テキストの最小色 | 比率 |
|---|
bg-white | text-gray-600 | 4.55:1 |
bg-white | text-gray-700 | 6.62:1 |
bg-gray-50 | text-gray-700 | 6.29:1 |
bg-gray-900 | text-gray-300 | 5.92:1 |
bg-blue-600 | text-white | 5.23:1 |
bg-red-600 | text-white | 4.54:1 |
bg-green-700 | text-white | 4.58:1 |
修正テンプレート
<button class="bg-blue-500 text-white px-4 py-2 rounded outline-none">
Submit
</button>
<button class="bg-blue-500 text-white px-4 py-2 rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2">
Submit
</button>
<p class="text-gray-400">Important information</p>
<p class="text-gray-700">Important information</p>
<div class="transition-transform duration-300 hover:scale-105">Card</div>
<div class="transition-transform duration-300 hover:scale-105 motion-reduce:transition-none motion-reduce:hover:scale-100">Card</div>