タブ・ツールバー
ツールバー
解説ありToolbar
ボタン群をまとめ、Tab 1回で入って矢印で移動できるようにするパターン。
ツールバー(Toolbar)とは?
ツールバーは、関連する操作ボタンを1か所にまとめて並べた UI です。 文章エディタの「太字・斜体・下線」、画像編集の道具箱、メディアプレーヤーの再生コントロールなどが典型例です。
ボタンを並べるだけなら簡単ですが、アクセシビリティの肝は「ツールバー全体を1つのグループ=1タブストップにする」ことです。
なぜアクセシビリティが大事なの?
- キーボードだけで操作する人。ボタンを1つずつ Tab で辿る作りだと、 道具が10個あれば本文に着くまで Tab を10回。ツールバーはTab 1回で入り、中は ←→ で移動するのが約束事です。
- スクリーンリーダーを使う人。
role="toolbar"とaria-labelがあると「○○ツールバー」とまとまりが伝わり、 中のボタン群を見通しよく扱えます。
この「グループで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> ボタンなのでフォーカスすらできません。
試してみよう: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 ボタンはフォーカス不可 —
tabindexもroleもないdivはキーボードで押せない。 - まとまりが伝わらない —
role="toolbar"/aria-labelがなく、ただのボタンの羅列に聞こえる。
実装チェックリスト
- ボタン群を
role="toolbar"+aria-labelで囲んでいる - ツールバー全体が 1タブストップ(Tab 1回で入る)
- roving tabindex:フォーカス中の1つだけ
tabindex="0"、他は-1 - ←→ でボタン移動、Home/End で端へ移動できる
- 各ボタンはネイティブの
<button>(divを使わない) - トグルボタンに
aria-pressedを付け、状態を更新している - 抜けて戻ると最後にフォーカスしていたボタンに戻る
- フォーカスが見える(フォーカスリングを消していない)