ARIA Authoring Practices Guideのページを参考に、タブの実装時にアクセシビリティの面を考慮してみます。
対応前
まずは特に考慮しないでタブの実装を行ってみます。
<div class="tab"> <div class="tablist"> <button type="button" class="tablist_switch is-open" data-tablist="tab-a">タブA</button> <button type="button" class="tablist_switch" data-tablist="tab-b">タブB</button> <button type="button" class="tablist_switch" data-tablist="tab-c">タブC</button> </div> <div class="tabpanel" data-tabpanel="tab-a"> <p>タブAの内容です。</p> </div> <div class="tabpanel" data-tabpanel="tab-b"> <p>タブBの内容です。</p> </div> <div class="tabpanel" data-tabpanel="tab-c"> <p>タブCの内容です。</p> </div> </div>
data属性を使ってタブボタンとパネルの紐づけを行う想定です。
次にCSSですが、タブの処理部分で必要な箇所のみの抜粋になります。
[data-tablist].is-open { color: red; } [data-tabpanel] { display: none; } [data-tabpanel].is-open { display: block; }
最後にタブの動作をJavaScriptで実装します。
const tabSwitchOpenClass = "is-open"; const tabPanelOpenClass = "is-open"; const tabSwitchData = "tablist"; const tabPanelData = "tabpanel"; document.addEventListener('DOMContentLoaded', function() { const $tabSwitch = document.querySelectorAll(`[data-${tabSwitchData}]`); // 初期表示の設定 const target = document.querySelector(`[data-${tabSwitchData}].${tabSwitchOpenClass}`) .getAttribute(`data-${tabSwitchData}`); tab_change(target); $tabSwitch.forEach(function(element) { // タブ切り替えのイベント設定 element.addEventListener('click', function(e) { const target = e.currentTarget.getAttribute(`data-${tabSwitchData}`); tab_change(target); }); }); }); // タブ切り替えの処理 function tab_change(target) { document.querySelector(`[data-${tabSwitchData}].${tabSwitchOpenClass}`) ?.classList.remove(tabSwitchOpenClass); document.querySelector(`[data-${tabSwitchData} = ${target}]`) ?.classList.add(tabSwitchOpenClass); document.querySelector(`[data-${tabPanelData}].${tabPanelOpenClass}`) ?.classList.remove(tabPanelOpenClass); document.querySelector(`[data-${tabPanelData} = ${target}]`) ?.classList.add(tabPanelOpenClass); }
これで簡易的なタブの実装ができました。
このタブをベースに、アクセシビリティを考慮してみます。
対応前のデモページ
要件
次に、ARIA Authoring Practices Guide (APG)のTabs Patternのページを参考に、実装する内容を確認します。
キーボード操作
- Tab:
- When focus moves into the tab list, places focus on the active
tab
element.- When the tab list contains the focus, moves focus to the next element in the page tab sequence outside the tablist, which is the tabpanel unless the first element containing meaningful content inside the tabpanel is focusable.
Tabキーでタブリスト(タブの切り替えボタン)に移動する際、アクティブなタブボタンにフォーカスする。
タブリストにフォーカスが含まれている場合、タブリスト外の次の要素(タブパネル)にフォーカスする。
- When focus is on a tab element in a horizontal tab list:
- Left Arrow: moves focus to the previous tab.
If focus is on the first tab, moves focus to the last tab.
Optionally, activates the newly focused tab (See note below).- Right Arrow: Moves focus to the next tab.
If focus is on the last tab element, moves focus to the first tab.
Optionally, activates the newly focused tab (See note below).
横並びのタブリスト内のタブボタンにフォーカスがある場合、
左矢印キーでフォーカスを前のタブに移動、フォーカスが最初のタブボタンにある場合は最後のタブに移動する。
右矢印キーでフォーカスを次のタブに移動、フォーカスが最後のタブボタンにある場合は最初のタブに移動する。
オプションで、新しくフォーカスされたタブボタンに紐づくタブパネルをアクティブにする。(推奨)
- When a tab list has its aria-orientation set to
vertical
:
- Down Arrow performs as Right Arrow is described above.
- Up Arrow performs as Left Arrow is described above.
縦並びのタブリスト(タブリストのaria-orientation属性にverticalが設定されている)の場合、下矢印キーは上記の右矢印と同じように機能、上矢印キーは左矢印と同じように機能する。
- When focus is on a tab in a tablist with either horizontal or vertical orientation:
- Space or Enter: Activates the tab if it was not activated automatically on focus.
- Home (Optional): Moves focus to the first tab.
Optionally, activates the newly focused tab (See note below).- End (Optional): Moves focus to the last tab.
Optionally, activates the newly focused tab (See note below).- Shift + F10: If the tab has an associated popup menu, opens the menu.
- Delete (Optional): If deletion is allowed, deletes (closes) the current tab element and its associated tab panel, sets focus on the tab following the tab that was closed, and optionally activates the newly focused tab.
If there is not a tab that followed the tab that was deleted, e.g., the deleted tab was the right-most tab in a left-to-right horizontal tab list, sets focus on and optionally activates the tab that preceded the deleted tab.
If the application allows all tabs to be deleted, and the user deletes the last remaining tab in the tab list, the application moves focus to another element that provides a logical work flow.
As an alternative to Delete, or in addition to supporting Delete, the delete function is available in a context menu.
タブリスト内のタブボタンにフォーカスがある場合、
スペースキーまたはエンターキーで、タブパネルをアクティブにする。(タブボタンをフォーカス時に紐づくタブパネルをアクティブにする設定をしていない場合)
オプションで、HOMEキーでフォーカスを最初のタブに移動、Endキーフォーカスを最後のタブに移動する。
Shiftキー + F10キーで、タブに関連付けられたポップアップメニューがある場合に開く。
Deleteキー(オプション)で削除が許可されている場合は、現在のタブボタンと紐づくタブパネルを削除もしくは閉じて、閉じたタブボタンの次のタブボタンにフォーカスを移動する。
削除された次のタブボタンがない場合、前のタブボタンにフォーカスを移動する。
すべてのタブの削除を許可していて、ユーザーがタブリストに残っている最後のタブボタンを削除した場合、別の要素にフォーカスを移動する。
WAI-ARIA
タブリスト(タブボタンのラップ要素)にはrole=tablistを設定する。
タブボタンにはrole=tabを設定して、role=tablistを持つ要素内に含める。
タブパネルにはrole=tabpanelを設定する。
- If the tab list has a visible label, the element with role
tablist
has aria-labelledby set to a value that refers to the labelling element.
Otherwise, thetablist
element has a label provided by aria-label.
タブリストに可視ラベルがある場合、タブリストにaria-labelledbyを設定する。
ない場合にはaria-labelを設定する。
- Each element with role
tab
has the property aria-controls referring to its associatedtabpanel
element.- The active
tab
element has the state aria-selected set totrue
and all othertab
elements have it set tofalse
.
各タブボタンに、紐づいているタブパネルを参照するaria-controls属性を設定する。
アクティブなタブボタンにはaria-selected=trueを、それ以外のタブボタンにはaria-selected=falseを設定する。
- Each element with role
tabpanel
has the property aria-labelledby referring to its associatedtab
element.
各タブパネルに、紐づいているタブボタンを参照するaria-labelledby属性を設定する。
- If a
tab
element has a popup menu, it has the property aria-haspopup set to eithermenu
ortrue
.
タブボタンにポップアップメニューがある場合、aria-haspopup属性にmenuまたはtrueを設定する。
- If the
tablist
element is vertically oriented, it has the property aria-orientation set tovertical
.
The default value ofaria-orientation
for atablist
element ishorizontal
.
縦並びのタブリストの場合、aria-orientation=verticalを設定する。
タブリストのaria-orientationはhorizontalがデフォルト値。
これらを踏まえて、今回は以下の内容を対応するようにしてみます。
- Tabキーでタブリスト(タブの切り替えボタン)に移動する際、アクティブなタブボタンにフォーカスする。
- タブリストにフォーカスが含まれている場合、タブリスト外の次の要素(タブパネル)にフォーカスする。
- 横並びのタブリスト内のタブボタンにフォーカスがある場合、
- 左矢印キーでフォーカスを前のタブに移動、フォーカスが最初のタブボタンにある場合は最後のタブに移動する。
- 右矢印キーでフォーカスを次のタブに移動、フォーカスが最後のタブボタンにある場合は最初のタブに移動する。
- オプションで、新しくフォーカスされたタブボタンに紐づくタブパネルをアクティブにする。(推奨)
- タブリスト内のタブボタンにフォーカスがある場合、
- オプションで、HOMEキーでフォーカスを最初のタブに移動、Endキーフォーカスを最後のタブに移動する。
- タブリスト(タブボタンのラップ要素)にはrole=tablistを設定する。
- タブボタンにはrole=tabを設定して、role=tablistを持つ要素内に含める。
- タブパネルにはrole=tabpanelを設定する。
- タブリストに可視ラベルがある場合、タブリストにaria-labelledbyを設定、ない場合にはaria-labelを設定する。
- 各タブボタンに、紐づいているタブパネルを参照するaria-controls属性を設定する。
- アクティブなタブボタンにはaria-selected=trueを、それ以外のタブボタンにはaria-selected=falseを設定する。
- 各タブパネルに、紐づいているタブボタンを参照するaria-labelledby属性を設定する。
WAI-ARIAの対応
まずはHTMLへの変更を中心に対応してみます。
<div class="tab"> <div class="tablist" role="tablist" aria-label="タブのラベル例" > <button type="button" class="tablist_switch" role="tab" id="tab-a" aria-controls="tabpanel-a" aria-selected="true" >タブA</button> <button type="button" class="tablist_switch" role="tab" id="tab-b" aria-controls="tabpanel-b" aria-selected="false" >タブB</button> <button type="button" class="tablist_switch" role="tab" id="tab-c" aria-controls="tabpanel-c" aria-selected="false" >タブC</button> </div> <div class="tabpanel" role="tabpanel" id="tabpanel-a" aria-labelledby="tab-a" > <p>タブAの内容です。</p> </div> <div class="tabpanel" role="tabpanel" id="tabpanel-b" aria-labelledby="tab-b" > <p>タブBの内容です。</p> </div> <div class="tabpanel" role="tabpanel" id="tabpanel-c" aria-labelledby="tab-c" > <p>タブCの内容です。</p> </div> </div>
HTMLの変更点は以下の通りです。
- タブリストにrole=tablistとaria-label属性を設定。
- タブボタンにrole=tabとid、aria-controls属性、aria-selected属性を設定。
- タブパネルにrole=tabpanelとaria-labelledby属性を設定。
CSSはdata属性を使った指定からrole属性を使った指定に変更して、タブボタンのアクティブはaria-selected属性を使うようにします。
[role="tab"][aria-selected="true"] { color: red; } [role="tabpanel"] { display: none; } [role="tabpanel"].is-open { display: block; }
最後にJavaScriptです。
前述の通り、data属性からrole属性を使った形にしている点と、アクティブな状態の確認にaria-selected属性を使っている点、タブボタンとパネルの紐づけをaria-controls属性で行っている点が主な変更点になります。
const tabPanelOpenClass = "is-open"; document.addEventListener('DOMContentLoaded', function() { const $tabSwitch = document.querySelectorAll('[role="tab"]'); // 初期表示の設定 const target = document.querySelector('[role="tab"][aria-selected="true"]') .getAttribute('aria-controls'); tab_change(target); $tabSwitch.forEach(function(element) { // タブ切り替えのイベント設定 element.addEventListener('click', function(e) { const target = e.currentTarget.getAttribute('aria-controls'); tab_change(target); }); }); }); // タブ切り替えの処理 function tab_change(target) { document.querySelector('[role="tab"][aria-selected="true"]') ?.setAttribute('aria-selected', 'false'); document.querySelector(`[role="tab"][aria-controls=${target}]`) ?.setAttribute('aria-selected', 'true'); document.querySelector(`[role="tabpanel"].${tabPanelOpenClass}`) ?.classList.remove(tabPanelOpenClass); document.querySelector(`[role="tabpanel"][id=${target}]`) ?.classList.add(tabPanelOpenClass); }
これで以下項目に対応する形での実装ができました。
WAI-ARIA対応のデモページ
- タブリスト(タブボタンのラップ要素)にはrole=tablistを設定する。
- タブボタンにはrole=tabを設定して、role=tablistを持つ要素内に含める。
- タブパネルにはrole=tabpanelを設定する。
- タブリストに可視ラベルがある場合、タブリストにaria-labelledbyを設定、ない場合にはaria-labelを設定する。
- 各タブボタンに、紐づいているタブパネルを参照するaria-controls属性を設定する。
- アクティブなタブボタンにはaria-selected=trueを、それ以外のタブボタンにはaria-selected=falseを設定する。
- 各タブパネルに、紐づいているタブボタンを参照するaria-labelledby属性を設定する。
キーボード操作の対応
最後に、以下のキーボード操作周りの対応を行います。
- Tabキーでタブリスト(タブの切り替えボタン)に移動する際、アクティブなタブボタンにフォーカスする。
- タブリストにフォーカスが含まれている場合、タブリスト外の次の要素(タブパネル)にフォーカスする。
- 横並びのタブリスト内のタブボタンにフォーカスがある場合、
- 左矢印キーでフォーカスを前のタブに移動、フォーカスが最初のタブボタンにある場合は最後のタブに移動する。
- 右矢印キーでフォーカスを次のタブに移動、フォーカスが最後のタブボタンにある場合は最初のタブに移動する。
- オプションで、新しくフォーカスされたタブボタンに紐づくタブパネルをアクティブにする。(推奨)
- タブリスト内のタブボタンにフォーカスがある場合、
- オプションで、HOMEキーでフォーカスを最初のタブに移動、Endキーフォーカスを最後のタブに移動する。
HTMLとCSSの変更はありません。
JavaScriptは以下の通りです。
const tabPanelOpenClass = "is-open"; document.addEventListener('DOMContentLoaded', function() { const $tabSwitch = document.querySelectorAll('[role="tab"]'); // 初期表示の設定 const target = document.querySelector('[role="tab"][aria-selected="true"]') .getAttribute('aria-controls'); tab_change(target); $tabSwitch.forEach(function(element) { // tabindexの初期設定 if(element.getAttribute('aria-selected') == 'true') { element.setAttribute('tabindex', 0); } else { element.setAttribute('tabindex', -1); } // タブ切り替えのイベント設定 element.addEventListener('keydown', tablist_move); element.addEventListener('click', function(e) { const target = e.currentTarget.getAttribute('aria-controls'); tab_change(target); }); }); }); // タブ切り替えの処理 function tab_change(target) { const $beforeBtn = document.querySelector('[role="tab"][aria-selected="true"]'); const $afterBtn = document.querySelector(`[role="tab"][aria-controls=${target}]`); const $beforePanel = document.querySelector(`[role="tabpanel"].${tabPanelOpenClass}`); const $afterPanel = document.querySelector(`[role="tabpanel"][id=${target}]`); // カレント状態とtabindexの切り替え $beforeBtn?.setAttribute('tabindex', -1); $beforeBtn?.setAttribute('aria-selected', 'false'); $afterBtn?.setAttribute('tabindex', 0); $afterBtn?.setAttribute('aria-selected', 'true'); $beforePanel?.removeAttribute('tabindex'); $beforePanel?.classList.remove(tabPanelOpenClass); $afterPanel?.setAttribute('tabindex', 0); $afterPanel?.classList.add(tabPanelOpenClass); } // タブボタン間の移動 function tablist_move(event) { const target = event.currentTarget; const tabBtnLength = document.querySelectorAll('[role="tab"]').length; let current = 0; let next = 0; document.querySelectorAll('[role="tab"]').forEach(function(element, index) { if(element === target) current = index; }); // キーに応じて移動先のタブボタンを決める const keycode = event.key; if(keycode === 'ArrowLeft') { next = current > 0 ? current - 1 : tabBtnLength - 1; } else if(keycode === 'ArrowRight') { next = current < tabBtnLength - 1 ? current + 1 : 0; } else if(keycode === 'Home') { next = 0; } else if(keycode === 'End') { next = tabBtnLength - 1; } else { return; } document.querySelectorAll('[role="tab"]')[next].focus(); const targetId = document.querySelectorAll('[role="tab"]')[next].getAttribute('aria-controls'); tab_change(targetId); event.stopPropagation(); event.preventDefault(); }
これでキーボード操作の対応ができました。
キーボード操作対応のデモページ
アクティブなタブボタンへのフォーカス移動と、タブパネルへのフォーカスはそれぞれtabindex属性を使って対応しています。
コメントが承認されるまで時間がかかります。