タブUIをちゃんと理解する
タブのUIについて実装する機会はちょこちょこあるものの、ちゃんと理解しようと思い調べた内容になります。
参考にしたもの
今回実装したものについて
コードについて
<div class="tab"> <ul class="tab-list" role="tablist"> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-1 is-active" aria-controls="panel1" aria-selected="true" role="tab" id="tab1" > タブ1 </button> </li> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-2" aria-controls="panel2" aria-selected="false" role="tab" id="tab2" > タブ2 </button> </li> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-3" aria-controls="panel3" aria-selected="false" role="tab" id="tab3" > タブ3 </button> </li> </ul> <div class="tab-panel js-pannel" id="panel1" role="tabpanel" aria-labelledby="tab1"> <p>パネル1</p> <p><a href="#">リンク1</a></p> </div> <div class="tab-panel js-pannel" id="panel2" role="tabpanel" aria-labelledby="tab2" hidden> <p>パネル2</p> <p><a href="#">リンク2</a></p> </div> <div class="tab-panel js-pannel" id="panel3" role="tabpanel" aria-labelledby="tab3" hidden> <p>パネル3</p> <p><a href="#">リンク3</a></p> </div> </div>
.tab { width: min(500px, 100%); } .tab-list { display: flex; } .tab-item + .tab-item { margin-left: 2px; } .tab-button { padding: 8px 16px; background: #fff; border-radius: 4px 4px 0 0; font-weight: bold; border-bottom: 1px solid #ddd; } .tab-button.is-active { color: #fff; background: #87ceeb; } .tab-panel { min-height: 100px; padding: 24px; background: #fff; }
// 今回使用するtab部分の要素を取得 const elemTabButton1 = document.querySelector('.js-button-1') const elemTabButton2 = document.querySelector('.js-button-2') const elemTabButton3 = document.querySelector('.js-button-3') // aria-controlsに記載されている内容を元に変更するid名を取得 const getIdName = (buttonElem) => { const idName = buttonElem.getAttribute('aria-controls') return idName } // クリックした時のイベント処理 const clickEvent = (elem) => { // クリック前にアクティブな要素を見つけて、activeを削除する const elemActiveButton = document.querySelector('button.is-active') if(elemActiveButton) { const activeIdName = getIdName(elemActiveButton) const elemActivePanel = document.getElementById(activeIdName) elemActiveButton.classList.remove('is-active') elemActiveButton.setAttribute('aria-selected', 'false') elemActivePanel.hidden = true } // クリックした要素に対して、activeにする const clickIdName = getIdName(elem) const elemclickPanel = document.getElementById(clickIdName) elem.classList.add('is-active') elem.setAttribute('aria-selected', 'true') elemclickPanel.hidden = false } // 各要素をクリックした時に関数を実行 elemTabButton1.addEventListener('click', () => { clickEvent(elemTabButton1) }, false) elemTabButton2.addEventListener('click', () => { clickEvent(elemTabButton2) }, false) elemTabButton3.addEventListener('click', () => { clickEvent(elemTabButton3) }, false)
実装した手順について
1. マークアップの作成
1-1. タブの全体を作成
<div class="tab"> <!-- タブの一覧 --> <ul class="tab-list" role="tablist"> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-1 is-active" aria-controls="panel1" aria-selected="true" role="tab" id="tab1" > タブ1 </button> </li> </ul> <!-- タブのパネル --> <div class="tab-panel js-pannel" id="panel1" aria-labelledby="tab1" role="tabpanel">パネル</div> </div>
1-2. タブの選択部分を作成
<ul class="tab-list" role="tablist"> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-1 is-active" aria-controls="panel1" aria-selected="true" role="tab" id="tab1" > タブ1 </button> </li> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-2" aria-controls="panel2" aria-selected="false" role="tab" id="tab2" > タブ2 </button> </li> <li class="tab-item" role="presentation"> <button type="button" class="tab-button js-button-3" aria-controls="panel3" aria-selected="false" role="tab" id="tab3" > タブ3 </button> </li> </ul>
対応した内容について
role-tablistについて
tablist
をHTMLに宣言することで、tab
のroleのものを紐付ける場合に使用します。
タブの1つ目にフォーカスを当てている場合
タブの3つ目にフォーカスを当てている場合
role-presentationについて
今回の場合、マークアップの見やすさからul>li
のものを使用していますが、「リストアイテム」等の読み込みが不要なため、presentation
を宣言しています。
role-tabについて
tablist
とtab
のroleを組み合わせることで要素の関連を結びつけることができるようになったり、VoiceOver等で「タブ」として認識されるようになります。(イメージについてはtablist
で共有した画像を参照)
aria-controlsについて
タブとタブパネルを結びつけるために宣言しています。
aria-selectedについて
タブの要素に対して、選択中かそうでないかをスクリーンリーダーに使えるために宣言しています。
1-3. タブの表示部分の作成
<div class="tab-panel js-pannel" id="panel1" role="tabpanel" aria-labelledby="tab1"> <p>パネル1</p> <p><a href="#">リンク1</a></p> </div> <div class="tab-panel js-pannel" id="panel2" role="tabpanel" aria-labelledby="tab2" hidden> <p>パネル2</p> <p><a href="#">リンク2</a></p> </div> <div class="tab-panel js-pannel" id="panel3" role="tabpanel" aria-labelledby="tab3" hidden> <p>パネル3</p> <p><a href="#">リンク3</a></p> </div>
対応した内容について
tabpanelについて
タブで表示されるコンテンツ部分をVoiceOver等で「タブパネル」のコンテンツと知らせるために宣言しています。
aria-labelledbyについて
どのタブパネルの要素が表示されているのかをタブの要素と結びつけることで以下のようにスクリーンリーダーが反応してくれるため、宣言しています。 aria-labelでも同じように読み込んでくれますが、html要素とaria-labelの2つを編集する必要があるため、aria-labelledbyの方を使用しています。
- aria-labelledbyなしの場合:
リンク、リンク1 、タブ1、タブパネル - aria-labelledbyありの場合:
リンク、リンク1 - aria-labelで
<div class="tab-panel js-pannel" id="panel1" role="tabpanel" aria-label="タブ1">{他と同じもの}</div>
のように使用した場合:
リンク、リンク1 、タブ1、タブパネル
hidden属性について
tab
で選択されていない要素のコンテンツの場合、ブラウザ上に表示・スクリーンリーダーでも読み込みは行われてほしくないため、非表示のコンテンツに対してはhidden
を宣言しています。
2. スタイルの追加
こちらは基本的なタブUIを実装したのみで、特別なことをしていないため、手順等は省略します。
3. タブとして動作をするようにする
今回のタブの要素を取得・クリックイベントの登録
// 今回使用するtab部分の要素を取得 const elemTabButton1 = document.querySelector('.js-button-1') const elemTabButton2 = document.querySelector('.js-button-2') const elemTabButton3 = document.querySelector('.js-button-3') // 各要素をクリックした時に関数を実行 elemTabButton1.addEventListener('click', () => { // ここにクリックしたときの関数を記載(今回は「clickEvent」の関数名) }, false) elemTabButton2.addEventListener('click', () => { // ここにクリックしたときの関数を記載(今回は「clickEvent」の関数名) }, false) elemTabButton3.addEventListener('click', () => { // ここにクリックしたときの関数を記載(今回は「clickEvent」の関数名) }, false)
現在activeなものを初期化する
const elemActiveButton = document.querySelector('button.is-active') if(elemActiveButton) { const activeIdName = getIdName(elemActiveButton) const elemActivePanel = document.getElementById(activeIdName) elemActiveButton.classList.remove('is-active') elemActiveButton.setAttribute('aria-selected', 'false') elemActivePanel.hidden = true }
ここの箇所で行っている内容については以下になります。
- アクティブな要素(
button.is-active
)を見つけて、存在したら以下を実行 - 現在アクティブなボタンのclass名から
is-active
の削除 → スタイルの変更をするため - 現在アクティブなボタンの
aria-selected
をfalseにする → スクリーンリーダーで選択中の要素でないことを知らせるため - 現在アクティブなパネルを非表示にする
クリックしたものに対して、activeにする
// クリックした要素に対して、activeにする const clickIdName = getIdName(elem) const elemclickPanel = document.getElementById(clickIdName) elem.classList.add('is-active') elem.setAttribute('aria-selected', 'true') elemclickPanel.hidden = false
ここの箇所で行っている内容については以下になります。
- クリックしたボタンのclass名から
is-active
の追加 → スタイルの変更をするため - クリックしたボタンの
aria-selected
をtrueにする → スクリーンリーダーで選択中の要素であることを知らせるため - クリックした要素に紐づくパネルを表示にする