ARIA タブ(roving tabindex)
Tab でタブ列に入り、← → Home End で移動。選択中のタブだけ tabindex=0(roving tabindex)、aria-selected と aria-controls で読み上げが「料金, タブ, 2/4, 選択」になる。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- 同じ領域で内容を切り替えるタブ
- Tab 1 回でタブ列を通り抜けさせたい画面
- 読み上げに「2/4, 選択」と位置を伝えたい場面
- 避ける場面
- ページ遷移するナビをタブに見せる
- パネルの読み込みが重いのに矢印で自動選択する
- パネルに操作要素も tabindex も無い構成
- 構成要素
- タブ列
- タブ
- パネル
- 読み上げ文の帯
- 持っている状態
- 通常
- フォーカス
- キーボード
Tab で選択中のタブへ入り、←→で移動と同時に選択、Home/End で端へ、次の Tab でパネルへ抜ける
- 支援技術
APG Tabs: role=tablist/tab/tabpanel、aria-selected、aria-controls と aria-labelledby で双方向に結ぶ、roving tabindex、パネル tabindex=0。非表示は hidden
- 動き
なし
デザインの要点
- タブ列は Tab で1回で通り抜けられるように、選択中のタブだけ tabindex=0・他は -1 にする(roving tabindex)
- ← → で移動したら同時に選択も切り替える(自動アクティベーション)。パネルの読み込みが重いときだけ Enter 待ちにする
- aria-controls と aria-labelledby で双方向に結ぶ。読み上げが「2/4」と位置を言えるのは role=tablist の中に role=tab があるから
- パネルに tabindex=0 を付ける。中に操作要素が無いパネルは、Tab で飛ばされて読めなくなる
プロのサイトでの実例
実例は「型の使われ方」の観察であり、コード・ロゴの転載ではありません。サイト側の更新で見え方が変わることがあります。
使いどころ
同じ領域の中で内容だけを差し替えるタブに使う。行き先がページではなく同一ページ内のパネルであることが条件で、URL が変わりブラウザの戻るで戻れる導線は、見た目がタブでも実体はナビゲーションなのでリンクのままにする(role=tablist を付けると読み上げが「タブ」と言い、戻れることが伝わらない)。パネルの中身が重く、切り替えのたびに通信が起きる場合は、矢印キーで即座に選択が変わる自動アクティベーションを避け、矢印は移動だけ・Enter で選択する手動アクティベーションにする。タブが2つしかないなら、セグメンテッドコントロールやトグルの方が意図に近いことも多い。
仕組み
骨格は roving tabindex。選択中のタブだけ tabindex=0、ほかは -1 にすることで、タブ列全体が Tab 1回で通り抜けられる1つの停止点になる。矢印キーの keydown で次の添字を計算し、preventDefault してから選択処理を呼び、aria-selected の付け替え・tabindex の付け替え・パネルの hidden の付け替え・フォーカス移動をまとめて行う。ArrowRight/ArrowLeft は剰余で端から端へ巡回し、Home/End は先頭と末尾へ飛ぶ。タブとパネルは aria-controls(タブ→パネル)と aria-labelledby(パネル→タブ)で双方向に結んであり、パネル側の tabindex=0 によって、操作要素が無いパネルでも Tab でフォーカスして本文を読める。見本は読み上げ文を組み立てて帯に出すので、「料金, タブ, 2/4, 選択」の位置情報がどこから来るかを目で確認できる。
実装の注意
崩れ方は3つある。ひとつは全タブに tabindex=0 を残す実装で、タブが8個あれば停止点も8個になり、本文へ辿り着くまでの Tab 回数がタブの数だけ増える。ふたつめは矢印キーで preventDefault を忘れる実装で、選択が変わると同時にページがスクロールする。みっつめはパネルの隠し方で、opacity:0 や height:0 で隠すと中身が Tab 順に残り、見えないパネルのリンクへフォーカスが飛ぶ(hidden 属性なら読み上げとフォーカス順の両方から同時に外れる)。さらに、矢印移動と同時に選択も切り替える自動アクティベーションは、パネルの読み込みが重いと端から端まで移動する間に全部読み込むことになるので、重い場合だけ手動へ切り替える判断が要る。
アクセシビリティ
WAI-ARIA Authoring Practices の Tabs パターンに沿っている。role="tablist" の直下に role="tab"、対応する領域に role="tabpanel" を置き、tablist には aria-label で「何のタブ列か」を与える。読み上げが「2/4」と位置を言えるのは、tablist の子として tab が並ぶ構造そのものから件数と位置が導かれるためで、テキストで書き足す必要はない。選択状態は aria-selected で伝え、下線や色だけに頼らない。パネルには tabindex="0" を付けて、リンクもボタンも無いパネルでもキーボードで内容へ到達できるようにする。非表示のパネルは hidden 属性で、読み上げとフォーカス順の両方から同時に外す。
調整して使う
まず選択の方式。既定は自動アクティベーション(矢印で移動=選択)で、APG もパネルの表示が軽いときはこちらを薦めている。重い場合は矢印を移動のみにし、Enter/Space で選択する。次に向きで、縦並びにするなら tablist へ aria-orientation="vertical" を付け、キー割り当ても ArrowUp/ArrowDown へ入れ替える(横のまま ↑↓ も効かせる折衷もよく使われる)。3つめは巡回の有無で、見本は端で折り返すが、折り返さず端で止める実装もどちらも APG の許容範囲。タブが画面幅に収まらないときは、横スクロールさせるより件数を減らすかドロップダウンへ切り替える方が操作しやすい。
コード(コピーして使えます)
<div class="atb-wrap">
<div class="atb-tablist" role="tablist" aria-label="プランの詳細" id="atb-tablist">
<button type="button" role="tab" id="atb-t1" aria-selected="true" aria-controls="atb-p1" tabindex="0">概要</button>
<button type="button" role="tab" id="atb-t2" aria-selected="false" aria-controls="atb-p2" tabindex="-1">料金</button>
<button type="button" role="tab" id="atb-t3" aria-selected="false" aria-controls="atb-p3" tabindex="-1">導入手順</button>
<button type="button" role="tab" id="atb-t4" aria-selected="false" aria-controls="atb-p4" tabindex="-1">FAQ</button>
</div>
<div class="atb-panel" role="tabpanel" id="atb-p1" aria-labelledby="atb-t1" tabindex="0">
<p>見積もり・請求・入金消込を1つの画面で。会計ソフトへは仕訳データとして連携します。</p>
</div>
<div class="atb-panel" role="tabpanel" id="atb-p2" aria-labelledby="atb-t2" tabindex="0" hidden>
<p><strong>¥1,980/月</strong>(1ユーザー・年払い)。請求書の発行枚数に上限はありません。</p>
</div>
<div class="atb-panel" role="tabpanel" id="atb-p3" aria-labelledby="atb-t3" tabindex="0" hidden>
<p>①取引先の CSV を取り込む → ②請求テンプレートを1つ作る → ③初回の請求書を送る。最短で当日に使い始められます。</p>
</div>
<div class="atb-panel" role="tabpanel" id="atb-p4" aria-labelledby="atb-t4" tabindex="0" hidden>
<p>途中解約は可能ですか — はい。月末までの利用分だけ請求され、データは 90 日間保持されます。</p>
</div>
<div class="atb-band">
<span class="atb-tag">読み上げ</span><span id="atb-sr">概要, タブ, 1/4, 選択</span>
<span class="atb-keys" aria-hidden="true"><kbd>←</kbd><kbd>→</kbd><kbd>Home</kbd><kbd>End</kbd></span>
</div>
</div>