フォーム・入力

リストボックス

解説あり

Listbox

選択肢のリストから選ぶ。単一/複数選択と矢印キー操作。

リストボックスとは?

リストボックスは、選択肢を一覧で表示し、その中から選ぶ UI です。 見た目はただのリストに似ていますが、「選べる項目の集まり」であり、 「いまどれが選ばれているか」を持つ点が普通のリストと違います。

ここでは1つだけ選ぶ(単一選択)リストボックスを扱います。 選択肢が少なく常に見えていてよい場面で使い、開閉が必要ならコンボボックス を検討します。

なぜアクセシビリティが大事なの?

これを満たすのが role="listbox" > role="option" の構造と、aria-selected、そしてキーボードフォーカス管理(ロービングタブインデックス または aria-activedescendant)です。

ライブデモ(推奨実装)

果物を1つ選ぶリストボックスです。リストにフォーカスを当ててから、矢印キーで操作してみてください。

アクセシブルなリストボックス(単一選択)
好きな果物を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" + 一意の id1つの選択肢。idaria-activedescendant から参照される。
各項目aria-selected="true | false"選択状態。選択の変化に合わせて必ず更新する。

実装:推奨パターン(Good)

良い例 / 推奨

role="listbox" の中に role="option" を並べ、aria-selectedaria-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 を付けただけのものです。マウスでは選べますが、フォーカスできず、矢印キーも効かず、選択状態も伝わりません。

div のリストで作った壊れたリストボックス
りんご
みかん
ぶどう
もも

試してみよう: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 がなく、どれが選ばれているか分からない。
  • 名前も位置もない — グループ名や「何個中何個目」が読み上げられない。

実装チェックリスト


原文(英語):Listbox Pattern — W3C APG(新しいタブで開きます)