タブ・ツールバー

ツールバー

解説あり

Toolbar

ボタン群をまとめ、Tab 1回で入って矢印で移動できるようにするパターン。

ツールバー(Toolbar)とは?

ツールバーは、関連する操作ボタンを1か所にまとめて並べた UI です。 文章エディタの「太字・斜体・下線」、画像編集の道具箱、メディアプレーヤーの再生コントロールなどが典型例です。

ボタンを並べるだけなら簡単ですが、アクセシビリティの肝は「ツールバー全体を1つのグループ=1タブストップにする」ことです。

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

この「グループで1タブストップ」を実現する仕組みがroving tabindex(フォーカスできる項目を常に1つだけにする手法)です。

ライブデモ(推奨実装)

下のツールバーは APG に沿った実装です。前後の入力欄と合わせてキーボードで操作してみてください。

アクセシブルなツールバー

試してみよう:Tab でツールバーに入る(1回で入れる)→ ← → でボタン間を移動 → Home / End で端へ → Enter / Space で切替 → もう一度 Tab で抜けて次の要素へ。

ポイント

オン/オフが切り替わるボタン(太字など)には aria-pressed を付けます。 スクリーンリーダーは「太字, 切り替えボタン, オン」のように 押下状態を読み上げます。

キーボード操作

キー動作必須/任意
Tabツールバーに入る/出る(ツールバー全体で1タブストップ)必須
/ 前 / 次のボタンへフォーカス移動必須
Home / End最初 / 最後のボタンへ移動任意(推奨)
Enter / Spaceフォーカス中のボタンを実行/切替必須

補足

ツールバーから Tab で出ると、次に戻ってきたときは最後にフォーカスしていたボタンに戻ります(roving tabindex でtabindex="0" の位置が移動するため)。これも APG が推奨する挙動です。

必要な WAI-ARIA / ロール

付ける場所属性 / ロール意味
ツールバーの箱role="toolbar"操作ボタンの集まりであることを伝える。
ツールバーの箱aria-label(または aria-labelledby「文字の書式」などツールバーの名前を付ける。
各ボタンtabindex="0 | -1"フォーカス中の1つだけ 0、他は -1(roving tabindex)。
トグルボタンaria-pressed="true | false"オン/オフが切り替わるボタンの状態を伝える。
区切り線role="separator" + aria-orientationボタンのグループを視覚的・意味的に区切る(任意)。

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

良い例 / 推奨

role="toolbar" でまとめ、中のボタンは roving tabindex で1つだけ tabindex="0"。矢印キーでフォーカスを移します。

マークアップ:

<div role="toolbar" aria-label="文字の書式">
  <!-- 最初のボタンだけ tabindex="0"、残りは -1(roving tabindex) -->
  <button type="button" aria-pressed="false" tabindex="0">太字</button>
  <button type="button" aria-pressed="false" tabindex="-1">斜体</button>
  <button type="button" aria-pressed="false" tabindex="-1">下線</button>
</div>

roving tabindex と矢印キーのスクリプト:

document.querySelectorAll('[role="toolbar"]').forEach((bar) => {
  const items = Array.from(bar.querySelectorAll('button'));

  function focusItem(i) {
    items.forEach((b, n) => (b.tabIndex = n === i ? 0 : -1)); // 1つだけ 0
    items[i].focus();
  }

  items.forEach((btn, i) => {
    // トグルボタンは aria-pressed を反転
    btn.addEventListener('click', () => {
      btn.setAttribute('aria-pressed',
        String(btn.getAttribute('aria-pressed') !== 'true'));
    });
    btn.addEventListener('keydown', (e) => {
      let next = null;
      if (e.key === 'ArrowRight') next = (i + 1) % items.length;
      else if (e.key === 'ArrowLeft') next = (i - 1 + items.length) % items.length;
      else if (e.key === 'Home') next = 0;
      else if (e.key === 'End') next = items.length - 1;
      if (next !== null) { e.preventDefault(); focusItem(next); }
    });
  });
});

アンチパターン(Bad)

下のツールバーは、ボタンがすべて個別のタブストップになっていて矢印キーが効きません。 さらに「左寄せ」は <div> ボタンなのでフォーカスすらできません

個別タブストップ+divボタンの壊れたツールバー
左寄せ
中央

試してみよう:Tab を押すたびに1ボタンずつ進み(道具が多いほど面倒)、← → では移動できません。「左寄せ」は Tab で飛ばされ、キーボードから押せません。

<!-- ❌ アンチパターン -->
<div class="toolbar">
  <!-- すべて個別のタブストップ。矢印キーで移動できず、Tab を何度も押す羽目に -->
  <button>太字</button>
  <button>斜体</button>
  <button>下線</button>
  <!-- さらに div ボタンはフォーカスすらできない -->
  <div class="btn" onclick="align('left')">左寄せ</div>
</div>

悪い例 / 避ける

この実装の問題点:

  • 1ボタン=1タブストップ — 道具が増えるほど Tab 連打が必要で、APG非準拠。
  • 矢印キーで移動できない — roving tabindex も role="toolbar" もない。
  • div ボタンはフォーカス不可tabindexrole もない div はキーボードで押せない。
  • まとまりが伝わらないrole="toolbar" / aria-label がなく、ただのボタンの羅列に聞こえる。

実装チェックリスト


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