フォーム・入力
リストボックス
解説ありListbox
選択肢のリストから選ぶ。単一/複数選択と矢印キー操作。
リストボックスとは?
リストボックスは、選択肢を一覧で表示し、その中から選ぶ UI です。 見た目はただのリストに似ていますが、「選べる項目の集まり」であり、 「いまどれが選ばれているか」を持つ点が普通のリストと違います。
ここでは1つだけ選ぶ(単一選択)リストボックスを扱います。 選択肢が少なく常に見えていてよい場面で使い、開閉が必要ならコンボボックス を検討します。
なぜアクセシビリティが大事なの?
- キーボードだけで操作する人。↑↓ で項目を移動し、Home/End で端へ飛べる必要があります。
<div>+onclickだと矢印キーが効かず、選べません。 - スクリーンリーダーを使う人。「好きな果物, リストボックス, りんご, 選択済み, 6個中1個目」 のように名前・役割・選択状態・位置が読み上げられます。 ただの
<div>では「選べる一覧」だと分かりません。
これを満たすのが role="listbox" > role="option" の構造と、aria-selected、そしてキーボードフォーカス管理(ロービングタブインデックス または aria-activedescendant)です。
ライブデモ(推奨実装)
果物を1つ選ぶリストボックスです。リストにフォーカスを当ててから、矢印キーで操作してみてください。
- りんご
- みかん
- ぶどう
- もも
- いちご
- メロン
試してみよう:Tab でリストにフォーカス → ↑ ↓ で選択を移動、Home / End で最初・最後へ。クリックでも選べます。
ポイント
このデモは aria-activedescendant 方式です。フォーカスはリスト (<ul>)に置いたまま、「いまどの option を指しているか」だけを属性で伝えます。 各 option に tabindex を配る「ロービングタブインデックス」方式でも実装できます。
キーボード操作
| キー | 動作 | 必須/任意 |
|---|---|---|
| Tab | リストボックスにフォーカスを当てる / 外す | 必須 |
| ↓ / ↑ | 次 / 前の選択肢へ移動(移動に合わせて選択を更新) | 必須 |
| Home / End | 最初 / 最後の選択肢へ移動 | 任意(推奨) |
必要な WAI-ARIA / ロール
| 付ける場所 | 属性 / ロール | 意味 |
|---|---|---|
| コンテナ | role="listbox" | 「選べる項目の集まり」であることを示す。 |
| コンテナ | aria-labelledby(or aria-label) | リストボックスの名前を与える。 |
| コンテナ | tabindex="0" + aria-activedescendant | リストにフォーカスを置き、現在位置の option id を指す(activedescendant 方式)。 |
| 各項目 | role="option" + 一意の id | 1つの選択肢。id は aria-activedescendant から参照される。 |
| 各項目 | aria-selected="true | false" | 選択状態。選択の変化に合わせて必ず更新する。 |
実装:推奨パターン(Good)
良い例 / 推奨
role="listbox" の中に role="option" を並べ、aria-selected と aria-activedescendant をキー操作で同期させます。
マークアップ:
<span id="lb-label">好きな果物を1つ選択</span>
<ul role="listbox"
id="lb-list"
tabindex="0"
aria-labelledby="lb-label"
aria-activedescendant="lb-opt-0">
<li role="option" id="lb-opt-0" aria-selected="true">りんご</li>
<li role="option" id="lb-opt-1" aria-selected="false">みかん</li>
<li role="option" id="lb-opt-2" aria-selected="false">ぶどう</li>
</ul>キーボード操作と選択の同期:
const list = document.getElementById('lb-list');
const options = [...list.querySelectorAll('[role="option"]')];
const select = (index) => {
options.forEach((opt, i) => {
opt.setAttribute('aria-selected', String(i === index));
});
const active = options[index];
list.setAttribute('aria-activedescendant', active.id); // どれに居るかを伝える
active.scrollIntoView({ block: 'nearest' });
};
const current = () =>
options.findIndex((o) => o.getAttribute('aria-selected') === 'true');
list.addEventListener('keydown', (e) => {
const i = current();
let next = null;
if (e.key === 'ArrowDown') next = Math.min(i + 1, options.length - 1);
if (e.key === 'ArrowUp') next = Math.max(i - 1, 0);
if (e.key === 'Home') next = 0;
if (e.key === 'End') next = options.length - 1;
if (next !== null) { e.preventDefault(); select(next); }
});
// クリックでも選択
options.forEach((opt, i) => opt.addEventListener('click', () => {
select(i);
list.focus();
}));アンチパターン(Bad)
下は <div> のリストに onclick を付けただけのものです。マウスでは選べますが、フォーカスできず、矢印キーも効かず、選択状態も伝わりません。
試してみよう:Tab でフォーカスできず、矢印キーで移動もできません。スクリーンリーダーでは「選べる一覧」だと認識されず、選択状態も読み上げられません。
<!-- ❌ アンチパターン:div のリスト + onclick -->
<div class="list">
<!-- role も aria-selected も無く、キーボードで一切操作できない -->
<div class="option" onclick="pick(this)">りんご</div>
<div class="option" onclick="pick(this)">みかん</div>
<div class="option" onclick="pick(this)">ぶどう</div>
</div>悪い例 / 避ける
この実装の問題点:
- キーボードで操作できない —
divはフォーカスを受け取れず、矢印キーも効かない。 - 役割が伝わらない —
role="listbox"/role="option"がなく「選べる一覧」だと認識されない。 - 選択状態が伝わらない —
aria-selectedがなく、どれが選ばれているか分からない。 - 名前も位置もない — グループ名や「何個中何個目」が読み上げられない。
実装チェックリスト
- コンテナに
role="listbox"と名前(aria-labelledby等)がある - 各項目は
role="option"+ 一意のidを持つ aria-selectedが選択状態と常に一致している- フォーカス管理がある(
aria-activedescendantまたはロービングタブインデックス) - ↑↓ で移動でき、Home/End も使える(推奨)
- クリックでも選択でき、フォーカスが見える