タブUIをアクセシブルにする
どうも、nano72mknです。
アクセシビリティを意識してタブUIを作ったので、実装時に調べたことやポイントをまとめます。
タブUIについて
まず、初めにタブUIと言われて思い浮かべるのは、この形だと思います。

このUIは、2つのパーツに分けることができます。
1つ目は、「タブ」と呼ばれるパーツ

2つ目は、「タブパネル」と呼ばれるパーツ

この2つのパーツをがっちゃんこして、タブUIは出来ています。
タブUIをアクセシブルにする
roleとaria属性を付与してアクセシビリティ対応をする。
roleを付与する
付与する必要があるものは下記の3つ
- tab
- tablist
- tabpanel
tabとtablist
タブにはtab、複数のタブを囲っている要素にはtablistのroleを付与する

<div id="tab">
  <div role="tablist">
    <button role="tab">Tab1</button>
    <button role="tab">Tab2</button>
    <button role="tab">Tab3</button>
  </div>
  ...
<div>
tabpanel
タブパネルにtabpanelのroleをつける

<div id="tab">
  ...
  <div role="tabpanel">Tab Panel 1</div>
  <div role="tabpanel">Tab Panel 2</div>
  <div role="tabpanel">Tab Panel 3</div>
</div>
支援技術に対応する
何のタブなのか、タブの状態はどうなっているかを支援技術に伝えるために、aria属性を付与します
 tablistにaria-labelをつける
タブにフォーカスした際に何のタブグループなのかを知らせるためのaria-labelを設定します。
<div id="tab">
  <div role="tablist" aria-label="hoge">
    <button role="tab">Tab1</button>
    <button role="tab">Tab2</button>
    <button role="tab">Tab3</button>
  </div>
  ...
<div>
上記の実装で、「Tab1」にフォーカスがあたった際に 「Tab1、選択中、タブ、1/3、hoge、 タブグループ」 と読み上げてくれます。
※ 「hoge」がaria-labelで指定したテキスト
 tabにaria-selectedをつける
どのタブが選択されているかを判定できるようにするために、aria-selectedを付与します。
また、別のタブを選択したときにaria-selectedの状態を切り替えます。
<div id="tab">
  <div role="tablist" aria-label="hoge">
    <button role="tab" aria-selected="true">Tab1</button>
    <button role="tab" aria-selected="false">Tab2</button>
    <button role="tab" aria-selected="false">Tab3</button>
  </div>
  ...
<div>
 tabとtabpanelを紐づける
紐づける為にやることが2つあります
- 
tabにaria-controlsを指定
- 
tabpanelにaria-labelledbyを指定
<div id="tab">
  <div role="tablist" aria-label="hoge">
    <button
      role="tab"
      id="tab-1"
      aria-selected="true"
      aria-controls="tabpanel-1">
      Tab1
    </button>
    ...
  </div>
  <div
    role="tabpanel"
    id="tabpanel-1"
    aria-labelledby="tab-1">
    Tab Panel 1
  </div>
  ...
<div>
aria-labelledbyの影響で、タブパネルにフォーカスを当てたときにも 「本文、Tab1、タブパネル」 とタブのラベルも読み上げてくれます。
 tabpanelにdisplay: none;をつける
表示されているタブパネル以外にはdisplay: none;を付与し非表示にします。
<div id="tab">
  ...
  <div role="tabpanel">Tab Panel 1</div>
  <div role="tabpanel" style="display: none;">Tab Panel 2</div>
  <div role="tabpanel" style="display: none;">Tab Panel 3</div>
</div>
※display: none;を付与しているときは、すでにアクセシビリティツリーから削除されているためaria-hiddenは不要です。
キーボードで操作できるようにする
WCAGの達成基準2.1.1で下記のように言われております。
達成基準 2.1.1 キーボード (レベル A): コンテンツのすべての機能は、個々のキーストロークに特定のタイミングを要することなく、キーボードインタフェースを通じて操作可能である。ただし、その根本的な機能が利用者の動作による終点だけではない軌跡に依存する入力を必要とする場合は除く。
この項目はレベルA(最低基準)なので、対応していきます。
タブUIでクリアするべきキーボードの操作は4つあり、JSで実装します。
 tabのフォーカスを左右キーで移動させる
左右の矢印キーを押した際に、タブのフォーカスを移動するようにします。
「Tab1」にフォーカスがあった場合に左キーを押したときには「Tab3」へ
「Tab3」にフォーカスがあった場合に右キーを押したときには「Tab1」へフォーカスが移るようにします。

vue.jsであれば、@keydown.rightと@keydown.leftを使うとサクッとできます。
 フォーカスがtablistの外から移動してくる場合、アクティブなtabに移動させる
これを実現するためには、tabindexを調整する必要があります。
tabindexは、-1が指定されていると順次ナビゲーションでは到達できなくなるので、その性質を使います。
<div id="tab">
  <div role="tablist" aria-label="hoge">
    <button role="tab" aria-selected="false" tabindex="-1">Tab1</button>
    <button role="tab" aria-selected="true" tabindex="0">Tab2</button>
    <button role="tab" aria-selected="false" tabindex="-1">Tab3</button>
  </div>
  ...
<div>
アクティブなタブはtabindexを0にし、それ以外はtabindexを-1にしてフォーカスが当たらないようにします。

外の要素から「Tab1」をスキップし、「Tab2」にフォーカスが移動することを確認できます。
 フォーカスがアクティブなtabにある場合、紐づいているtabpanelに移動させる
tabindex="0"を付与するだけで対応できます。
<div id="tab">
  ...
  <div role="tabpanel" tabindex="0">Tab Panel 1</div>
  <div role="tabpanel" tabindex="0">Tab Panel 2</div>
  <div role="tabpanel" tabindex="0">Tab Panel 3</div>
</div>

フォーカスが「Tab2」から「Tab3」ではなく、直接紐づいているタブへ移動しているのを確認できます。
 おまけ: tabにフォーカスが当たっている時にDeleteキーを押したら削除
tabが削除可能な場合は、Deleteキーで選択されているタブを削除できるようにする必要があります。
まとめ
上記対応を行うと、下記codepenのようになります。
以上が、タブUIをアクセシブルにするためのポイントでした。
UIライブラリを使っていると、わからないような細かい対応もあり、とても楽しく実装できました。
また、何か間違いや足りない対応などがありましたら、お気軽にコメントしていただけると嬉しいです!
最後まで読んでいただきありがとうございました。
参考
サクッと説明しちゃっているので、詳しく知りたい方は、下記参考記事を読んでみてください!





Discussion
こんにちは!
アクセシビリティを意識されていて素晴らしい実装だと思います!
提案として、非表示は
display:noneではなくhidden="until-found"を利用するのをオススメします。until-foundの存在を初めて知りました!ページ内検索で引っかけることが出来る事ができるのは便利ですし、
safari等非対応環境でも既存の
hiddenと同じ挙動なのも安心ですねとても、勉強になります!
もっと
until-foundについて調べてみます!ありがとうございました🙇