jackmiwamiwa devblog

フロントエンドをメインに行ったことをまとめていくサイトになります。

タブUIをちゃんと理解する

タブのUIについて実装する機会はちょこちょこあるものの、ちゃんと理解しようと思い調べた内容になります。

参考にしたもの

qiita.com

kt-media.blog

ics.media

今回実装したものについて

コードについて

  <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について

developer.mozilla.org

tablistをHTMLに宣言することで、tabのroleのものを紐付ける場合に使用します。

タブの1つ目にフォーカスを当てている場合

タブの3つ目にフォーカスを当てている場合

role-presentationについて

developer.mozilla.org

今回の場合、マークアップの見やすさからul>liのものを使用していますが、「リストアイテム」等の読み込みが不要なため、presentationを宣言しています。

role-tabについて

developer.mozilla.org

tablisttabのroleを組み合わせることで要素の関連を結びつけることができるようになったり、VoiceOver等で「タブ」として認識されるようになります。(イメージについてはtablistで共有した画像を参照)

aria-controlsについて

developer.mozilla.org

タブとタブパネルを結びつけるために宣言しています。

aria-selectedについて

developer.mozilla.org

タブの要素に対して、選択中かそうでないかをスクリーンリーダーに使えるために宣言しています。

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について

developer.mozilla.org

タブで表示されるコンテンツ部分をVoiceOver等で「タブパネル」のコンテンツと知らせるために宣言しています。

aria-labelledbyについて

developer.mozilla.org

どのタブパネルの要素が表示されているのかをタブの要素と結びつけることで以下のようにスクリーンリーダーが反応してくれるため、宣言しています。 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属性について

developer.mozilla.org

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にする → スクリーンリーダーで選択中の要素であることを知らせるため
  • クリックした要素に紐づくパネルを表示にする