サイトにはアクセシビリティのコースがありますが、Reactにはそこで扱っていない問題があります。原因は1つです。
ページを読み直さずに中身だけ書きかえるから、ブラウザが本来やってくれることが起きません。
問題①:移動してもフォーカスが動かない
<Link>で詳細ページへ移ると、見た目は変わるのにフォーカスは押したリンクの位置に残ります。
- キーボードだけで操作する人は、新しいページの先頭ではなく前のページの途中から続きを操作することになる
- 読み上げソフトはページが変わったことに気づかない
対処は、移動したら見出しにフォーカスを移すことです。
const headingRef = useRef<HTMLHeadingElement>(null)
useEffect(() => {
headingRef.current?.focus()
}, [id]) // URLのidが変わるたびに実行
<h1 ref={headingRef} tabIndex={-1} className="focus:outline-2 focus:outline-sky-500">
{bird.name}
</h1>tabIndex={-1}が必要です。見出しは本来フォーカスできない要素なので、「プログラムからならフォーカスできる」という印を付けます(-1なのでTabキーの順番には入りません)。
問題②:静かに変わったところが伝わらない
検索の件数のように、押した場所とは別のところが変わる表示があります。目で見ていれば分かりますが、読み上げでは何も起きません。
<p aria-live="polite">{shownBirds.length}羽みつかりました</p>aria-live="polite"を付けると、中身が変わったときに読み上げソフトが知らせてくれます。politeは「いま読んでいることが終わったら」という意味で、ふつうはこちらを使います。
| 値 | 動き | 使う場面 |
|---|---|---|
polite | 読んでいる途中は割りこまない | 件数・保存しました・検索結果 |
assertive | すぐ割りこむ | 重大なエラーのみ |
問題③:divで作ったボタン
Reactでは何にでもonClickを付けられるので、つい書いてしまいます。
<div onClick={handleClick}>おす</div> // ✕
<button type="button" onClick={handleClick}>おす</button> // ○<button>にするとまとめて付いてくるものがあります。
- Tabキーで到達できる
- Enter・Spaceで押せる
- 読み上げで「ボタン」と分かる
disabledが効く
divで作ると、これらを全部自分で足すことになります(tabIndex・role="button"・onKeyDown…)。素直に<button>を使うのが、いちばん短くて確実です。
問題④:文字のないボタン
stateの回で付けたaria-labelが、これにあたります。
<button aria-label={`${name}をお気に入りにする`}>{isFavorite ? '★' : '☆'}</button>★だけのボタンは、読み上げでは「星印」か、何も読まれないかのどちらかです。何のボタンか分かりません。
アイコンだけのボタンには必ずラベル——これは実務のレビューでも定番の指摘です。
問題⑤:モーダルの中に閉じこめる
自分でモーダル(ダイアログ)を作ると、こうなりがちです。
- 開いてもフォーカスが中に移らない
- Tabで後ろの画面に抜けてしまう
- Escで閉じられない
- 閉じたあと、どこにフォーカスが戻るか決まっていない
これを全部自分で書くのは大変なので、<dialog>要素か、アクセシビリティ対応のライブラリ(Radix UI・React Ariaなど)を使うのが現実的です。
const dialogRef = useRef(null)
<button onClick={() => dialogRef.current?.showModal()}>ひらく</button>
<dialog ref={dialogRef}>
…
<button onClick={() => dialogRef.current?.close()}>とじる</button>
</dialog><dialog>のshowModal()なら、フォーカスの閉じこめ・Escで閉じる・背景の操作を止めるがブラウザ側で効きます。
確かめ方
キーボードだけで触るのがいちばん確実です。
- マウスから手を離す
- Tabだけで全部の操作に到達できるか
- いまどこにいるか、見て分かるか(フォーカスの枠が消えていないか)
- Enter・Spaceで押せるか、Escで閉じられるか
機械的なチェックはLighthouse(Chromeの開発者ツール)やaxe DevTools拡張で拾えます。ただしフォーカスの流れは機械には判定できないので、手で触る工程は残ります。
アクセシビリティキーボードだけで使えるページフォーカスの見た目(:focus-visible)はアクセシビリティコースのこの回に。Reactでも書き方は同じです公式ドキュメントも見てみる
ja.react.dev外部サイト<div> などの一般的なコンポーネント – React公式の共通propsのリファレンス。aria-*属性がそのまま書けること、キャメルケースにならない例外であることが書かれていますこのレッスンのまとめ
- Reactはページを読み直さないので、ブラウザ任せにできていたことが起きなくなる
- 移動したら見出しにフォーカスを移す(
useRef+tabIndex={-1}) - 静かに変わる表示には
aria-live="polite"。assertiveは緊急時だけ div+onClickにしない。<button>は到達・キー操作・読み上げがまとめて付く- アイコンだけのボタンには
aria-label——テストのしやすさにも直結する - モーダルは
<dialog>かライブラリに任せる(フォーカスの閉じこめは自作しない) - 点検はキーボードだけで触る。機械的なチェックはLighthouseやaxeで
次は、動き(アニメーション)です。この回で扱った「動きを減らす設定」とも地続きです。