【アクセシビリティ】アクセシビリティを意識したタブUIの作り方


## はじめに

![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390720/92181a22-3f42-340e-f418-a9d9be65f451.png)

みなさんはちゃんとアクセシビリティを意識して開発できていますか？

必要なところに `role属性` を記述したり、tabキーでフォーカスが当たるようにしたりなど、意識しないといけないことも多いです。
そのため、完璧にやろうとするのは難しいと思います。

ただ、モーダルUIやタブUIといったコンポーネントごとに区切って、アクセシビリティを理解しておけば、実装するタイミングに思い出しやすくアクセシビリティについて意識しやすいです。

そのため、この記事ではタブUIのコンポーネントに焦点を当てて、
アクセシビリティを意識したタブUIの実装方法とタブUIで意識した方がいいアクセシビリティを解説しようと思います。

※ アクセシビリティの基本については以下の記事をご参照ください。

https://qiita.com/degudegu2510/items/dd072655880adbe3f58c

##  アクセシビリティを意識したタブUIを実装する
では早速、アクセシビリティを意識したタブUIについて解説していきます。

### 1. HTMLを実装する
HTMLから実装していきます。

タブUIのHTMLで意識したアクセシビリティのポイントは、以下の通りです。

1. role属性を明記すること
      - role属性を明記することで、スクリーンリーダーなどでタブやタブパネルであることを読み上げてくれます。
      - 例)
        - タブの`<button>`をラップしている要素には、`role="tablist"`を書く
        - タブの`<button>`には、`role="tab"`を書く
        - タブのパネルには、`role="tabpanel"`を書く
2. aria属性を明記して、`tablist`と`tabpanel`を紐づけること
      - タブには、タブパネルを制御する役割があり、タブパネルには、どのタブの内容なのかを示す必要があるので、aria属性を明記して、`tablist`と`tabpanel`を紐づけます。
      - 例)
        - タブの`<button>`の`id`とタブのパネルの`aria-labelledby`に同じ値を書く
        - タブのパネルの`id`とタブの`<button>``aria-controls`に同じ値を書く
3. 選択している要素と選択していない要素を明確にすること
      - 選択している要素を明確にすることで、どんな人にも選択していることを伝えます。
      - 例)
        - 選択しているタブの`<button>`に`aria-selected="true"`を書く
        - 選択していないタブの`<button>`に`aria-selected="false"`を書く
        - 選択していないタブのパネルには`hidden`を書く
4. tabindexをコントロールすること
      - タブリストでは、`←キー`・`→キー`を使ってパネルを切り替えるため、選択しているタブだけにフォーカスが当たるようにします。
      - タブパネルにもちゃんとフォーカスが当たるようにします。
      - 例)
        - 選択しているタブに `tabindex="0"`を書く
        - 選択していないタブに `tabindex="-1"`を書く
5. 選択していないタブパネルに`hidden`を明記する。
      - 選択していないタブパネルに`hidden`を明記することで、表示されなくなります。


これらを意識して描いたHTMLは↓こちらになります。

```tab.html
<div class="tab">
  <div role="tablist">
    <button role="tab" id="tabA" aria-controls="panelA" aria-selected="true"  tabindex="0">
      タブA
    </button>
    <button role="tab" id="tabB" aria-controls="panelB" aria-selected="false" tabindex="-1">
      タブB
    </button>
    <button role="tab" id="tabC"aria-controls="panelC" aria-selected="false" tabindex="-1">
      タブC
    </button>
  </div>
  <div role="tabpanel" id="panelA" aria-labelledby="tabA" tabindex="0">
    <p>パネルAを表示しています。</p>
  </div>
  <div role="tabpanel" id="panelB" aria-labelledby="tabB" tabindex="0" hidden>
    <p>パネルBを表示しています。</p>
  </div>
  <div role="tabpanel" id="panelC" aria-labelledby="tabC" tabindex="0" hidden>
    <p>パネルCを表示しています。</p>
  </div>
</div>
```

codepenで実装したものは↓こちらです。

<p class="codepen" data-height="300" data-slug-hash="GRXwepz" data-user="sp_degu" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sp_degu/pen/GRXwepz">
  tab ui</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### 2. CSSを実装する
タブUIのCSSにおけるアクセシビリティは、コントラストとかに当たるので、詳しく説明するのは省略します。

今回は、↓こんな感じのスタイルを当てました。

```sample.css
.tab {
  width: 100%;
  max-width: 400px;
}

div[role="tablist"] {
  display: flex;
}

button[role="tab"] {
  border: none;
  background-color: #576066;
  border-radius: 8px 8px 0 0;
  font-size: 14px;
  font-weight: bold;
  padding: 4px 12px;
}

button[role="tab"][aria-selected="true"] {
  background-color: #FFFFFF;
}

button[role="tab"][aria-selected="false"]:hover {
  background-color: #FFFFFF;
  cursor: pointer;
}

div[role="tabpanel"] {
  background-color: #FFFFFF;
  border-radius: 0 8px 8px 8px;
  padding: 32px 16px;
}

p {
  font-size: 20px;
  font-weight: bold;
  margin: 0;
}

body {
  background-color: #212529;
  display: grid;
  height: calc(100vh - 16px);
  margin: 0;
  padding: 8px;
  place-items: center;
  width: calc(100% - 16px);
}
```

codepenで実装したものは↓こちらです。

<p class="codepen" data-height="300" data-slug-hash="qBMQvNo" data-user="sp_degu" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sp_degu/pen/qBMQvNo">
  tab ui</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### 3. タブのボタンを押して、パネルが切り替わるようにする。

1. 各タブにclickイベントを追加する
      - タブをクリックした時 `changeTabs` という関数が動くように、それぞれのタブにclickイベントを追加します。
2. クリックした要素に `aria-selected="true"`を追加する
      - 一度全部の`aria-selected`を`false`にします。
      - その後選択した要素だけ`aria-selected`を`true`に設定します。
3. クリックした要素に紐付いたタブパネルの`hidden`を削除する
      - 一度全部のタブパネルを`hidden`を設定します。
      - その後選択した要素だけ`.removeAttribute("hidden")`で`hidden`を削除します。


```sample.js
window.addEventListener("DOMContentLoaded", () => {
  const tabs = document.querySelectorAll('[role="tab"]');
  const tabList = document.querySelector('[role="tablist"]');

  // 各タブに click イベントを追加します
  tabs.forEach(tab => {
    tab.addEventListener("click", changeTabs);
  });
})

function changeTabs(e) {
  const target = e.target;
  const parent = target.parentNode;
  const grandparent = parent.parentNode;

  // タブから現在すべての選択状態を取り除きます
  parent
    .querySelectorAll('[aria-selected="true"]')
    .forEach(t => t.setAttribute("aria-selected", false));

  // このタブを選択されたタブとして設定します
  target.setAttribute("aria-selected", true);

  // すべてのタブパネルを非表示にします
  grandparent
    .querySelectorAll('[role="tabpanel"]')
    .forEach(p => p.setAttribute("hidden", true));

  // 選択されたパネルを表示します
  grandparent.parentNode
    .querySelector(`#${target.getAttribute("aria-controls")}`)
    .removeAttribute("hidden");
}
```

codepenで実装したものは↓こちらです。

<p class="codepen" data-height="300" data-slug-hash="KKxrENz" data-user="sp_degu" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sp_degu/pen/KKxrENz">
  tab ui</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

### 4. タブをキーボードで操作できるようにする

1. 最初に表示させるタブを指定します。
      - どのタブを表示しているかを制御するため、`tabFocus` を定義し、初期値を 最初に表示させるタブを指定します。
2. `←`・`→`キーが押されたことを検知する
      - `addEventListener`の`keydown`でキーボードが押されていることを検知します。
      - `←`のkeyCodeは37で、`→`のkeyCodeは39なので、if分を使って条件を分岐させます。
      - `←`・`→`キーが押された、表示されているタブにフォーカスが当たらないようにします。
3. `←`・`→`キーが押した時の処理を書く
      - `←`キーが押されたら、`tabFocus`の値を1減らします。
        - 表示されているタブが最初のタブ(tabFocusが0以下)なら、最後のタブにフォーカスが当たるようにします。
      - `→`キーが押されたら、`tabFocus`の値を1増やします。
        - 表示されているタブが最後のタブ(tabFocusがtabs.length以上)なら、最初のタブにフォーカスが当たるようにします。
4. `tabFocus`の値に合わせて、フォーカスが当たるようにする
      - `tabFocus`の値に合わせ、`tabinde="0"`を指定して、フォーカスが当たるようにします。

```sample.js
window.addEventListener("DOMContentLoaded", () => {
  const tabs = document.querySelectorAll('[role="tab"]');
  const tabList = document.querySelector('[role="tablist"]');

  // tabListの中で表示されているタブを指定するようの定数。
  let tabFocus = 0;
  tabList.addEventListener("keydown", e => {
    //← →を押したら
    if (e.keyCode === 37 || e.keyCode === 39) {
      tabs[tabFocus].setAttribute("tabindex", -1);

      if (e.keyCode === 37) {
        // ← を押したら
        tabFocus--;
        // 最初にいる場合は、最後に移動します
        if (tabFocus < 0) {
          tabFocus = tabs.length - 1;
        }
      } else if (e.keyCode === 39) {
        // → を押したら
        tabFocus++;
        // 最後にいる場合は、最初に移動します
        if (tabFocus >= tabs.length) {
          tabFocus = 0;
        }
      }
      tabs[tabFocus].setAttribute("tabindex", 0);
      tabs[tabFocus].focus();
    }
  })
})

// ...以下同じ。
```

codepenで実装したものは↓こちらです。
<p class="codepen" data-height="300" data-slug-hash="dyqQQLE" data-user="sp_degu" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sp_degu/pen/dyqQQLE">
  tab ui</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### 5. 完成
完成したのが↓こちらになります。

<p class="codepen" data-height="300" data-slug-hash="dyqQQLE" data-user="sp_degu" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sp_degu/pen/dyqQQLE">
  tab ui</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


## まとめ

この記事ではタブUIのコンポーネントに焦点を当てて、アクセシビリティを意識したタブUIの実装方法とタブUIで意識した方がいいアクセシビリティを解説しました。

このタブUIのアクセシビリティは、できていないサイトが多いので、ぜひやっていきたいです。
(Qiitaは順次反映させていきます。)

ぜひこの記事をストックして、タブUIを実装する時にこの記事・アクセシビリティについて思い出してもらえると嬉しいです。
他のUIのアクセシビリティについても解説していく予定なので、お楽しみにしていてください。

---

最後まで読んでくださってありがとうございます！

普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。

https://twitter.com/degudegu2510