タブ・ツールバー
タブ
解説ありTabs
同じ領域で内容を切り替える。矢印キー移動・aria-selected・roving tabindex。
タブ(Tabs)とは?
タブは、同じ場所に複数の「パネル(内容のかたまり)」を重ねて置き、 上部の見出し(タブ)を選ぶことで表示するパネルを切り替える UI です。 設定画面、商品詳細、ダッシュボードなど「関連する情報をスペースを節約して並べたい」場面でよく使われます。
見た目は「ボタンが並んでいるだけ」ですが、支援技術には「これはタブの集まりで、いまどれが選ばれているか」を正しく伝える必要があります。
なぜアクセシビリティが大事なの?
- キーボードだけで操作する人。タブが
<div>だとTab でフォーカスできず切り替えられません。また、タブ全体が1つのグループとして まとまっていないと、タブの数だけ Tab を押す羽目になり煩雑です。 - スクリーンリーダーを使う人。
role="tab"やaria-selectedがないと「タブである」ことも「いまどれが選択中か」も伝わらず、 ただのリンクやテキストの羅列に聞こえてしまいます。
ポイントは 「タブ全体で1タブストップ(roving tabindex)」 と「aria-selected で選択状態を伝える」の2つです。
ライブデモ(推奨実装)
下のタブは APG に沿った実装です。マウスを使わずキーボードで操作してみてください。
表示名やアイコンなど、公開プロフィールを編集します。
パスワードの変更や2段階認証の設定を行います。
メールやプッシュ通知の受け取り方を選びます。
試してみよう:Tab で選択中のタブに入る → ← → でタブを移動(同時にパネルも切替)→ Home / End で最初・最後へ → もう一度 Tab でパネルへ。
ポイント
スクリーンリーダーでタブにフォーカスすると「プロフィール, タブ, 選択済み, 3個中1個目」のように役割・選択状態・位置が読み上げられます。これが role="tab" とaria-selected の効果です。
キーボード操作
| キー | 動作 | 必須/任意 |
|---|---|---|
| Tab | タブリストに入る/出る(リスト全体で1タブストップ) | 必須 |
| ← / → | 前 / 次のタブへ移動(自動アクティベーションでは同時に選択) | 必須 |
| Home / End | 最初 / 最後のタブへ移動 | 任意(推奨) |
| Enter / Space | 手動アクティベーション時にフォーカス中のタブを選択 | 任意 |
補足
パネルの中身がすぐ表示できる軽い内容なら、矢印キーで移動と同時に選択する 「自動アクティベーション」が快適です(このデモもそれ)。表示に通信や重い処理が伴う場合は、Enter/Space で確定する「手動アクティベーション」にします。
必要な WAI-ARIA / ロール
| 付ける場所 | 属性 / ロール | 意味 |
|---|---|---|
| タブを囲む箱 | role="tablist" + aria-label | タブの集まりであることと、その名前を伝える。 |
| 各タブ | role="tab" | タブであることを伝える。中身は <button> 推奨。 |
| 各タブ | aria-selected="true | false" | いま選ばれているタブはどれか。切替時に必ず更新する。 |
| 各タブ | aria-controls="パネルのid" | そのタブが制御するパネルを示す。 |
| 各タブ | tabindex="0 | -1" | 選択タブだけ 0、他は -1(roving tabindex)。 |
| 各パネル | role="tabpanel" + aria-labelledby="タブのid" | 対応するタブを名前として持つパネル。 |
| 各パネル | tabindex="0" | パネル内にフォーカス可能要素がない場合でも、パネル自体に Tab で入れるようにする。 |
| 非表示パネル | hidden | 選ばれていないパネルは隠し、読み上げ/操作の対象外にする。 |
実装:推奨パターン(Good)
良い例 / 推奨
タブは <button role="tab">、パネルは role="tabpanel"。 選択状態を aria-selected と roving tabindex で同期させます。
マークアップ:
<div class="tabs">
<div role="tablist" aria-label="アカウント設定">
<button type="button"
role="tab"
id="tabs-tab-profile"
aria-selected="true"
aria-controls="tabs-panel-profile"
tabindex="0">プロフィール</button>
<button type="button"
role="tab"
id="tabs-tab-security"
aria-selected="false"
aria-controls="tabs-panel-security"
tabindex="-1">セキュリティ</button>
</div>
<div role="tabpanel"
id="tabs-panel-profile"
aria-labelledby="tabs-tab-profile"
tabindex="0">プロフィールの内容…</div>
<div role="tabpanel"
id="tabs-panel-security"
aria-labelledby="tabs-tab-security"
tabindex="0"
hidden>セキュリティの内容…</div>
</div>切替のスクリプト(aria-selected・tabindex・hidden を1か所で同期):
document.querySelectorAll('[data-tabs]').forEach((root) => {
const tabs = Array.from(root.querySelectorAll('[role="tab"]'));
function select(tab) {
tabs.forEach((t) => {
const selected = t === tab;
// aria-selected と roving tabindex を同期させる
t.setAttribute('aria-selected', String(selected));
t.tabIndex = selected ? 0 : -1; // 選択タブだけ Tab で入れる
const panel = document.getElementById(t.getAttribute('aria-controls'));
if (panel) panel.hidden = !selected;
});
}
tabs.forEach((tab, i) => {
tab.addEventListener('click', () => select(tab));
tab.addEventListener('keydown', (e) => {
let next = null;
if (e.key === 'ArrowRight') next = (i + 1) % tabs.length;
else if (e.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = tabs.length - 1;
if (next !== null) {
e.preventDefault();
tabs[next].focus(); // フォーカス移動
select(tabs[next]); // 同時にそのタブを選択(自動アクティベーション)
}
});
});
});アンチパターン(Bad)
下は <div> と onclick だけで作ったタブです。マウスでは切り替わりますが、キーボードでは一切操作できません。
表示名やアイコンなど、公開プロフィールを編集します。
パスワードの変更や2段階認証の設定を行います。
メールやプッシュ通知の受け取り方を選びます。
試してみよう:Tab を押してもタブにフォーカスが当たらず、← → でも切り替わりません。スクリーンリーダーは「タブ」とも「選択中」とも認識できません。
<!-- ❌ アンチパターン -->
<div class="tabs">
<div class="tablist">
<!-- div + onclick。role も aria-selected もキーボードもない -->
<div class="tab" onclick="show('p1')">プロフィール</div>
<div class="tab" onclick="show('p2')">セキュリティ</div>
</div>
<div id="p1">プロフィールの内容…</div>
<div id="p2" style="display:none">セキュリティの内容…</div>
</div>悪い例 / 避ける
この実装の問題点:
- キーボードで操作できない —
divはフォーカスを受け取れず、矢印キーも効かない。 - タブだと伝わらない —
role="tablist"/role="tab"がなく、ただのテキストに聞こえる。 - 選択状態が伝わらない —
aria-selectedがなく、いまどれが開いているか分からない。 - パネルと関連付いていない —
aria-controls/aria-labelledbyがない。
実装チェックリスト
- タブを
role="tablist"+aria-labelで囲んでいる - 各タブは
<button role="tab">である aria-selectedが選択状態と常に一致している- 選択タブだけ
tabindex="0"、他は-1(roving tabindex でタブ全体が1タブストップ) - ←→ でタブ移動、Home/End で端へ移動できる
- 各タブ
aria-controls、各パネルrole="tabpanel"+aria-labelledby+tabindex="0" - 非表示パネルは
hiddenで読み上げ/操作から外れている - フォーカスが見える(フォーカスリングを消していない)