会社の後輩コーダーが、次の案件で「CSSだけで作れるタブメニュー」を使う予定です!と言って、紹介記事を教えてくれました。
そのサンプルコードを見てみましたが、アクセシビリティ面に重大な問題があります。その記事だけではなく、「CSS タブ」などで検索して1ページ目に出てくる記事は、すべて同様の手法を使っていました。
私としては、後輩にも、令和のすべてのコーダーにもこの手法を使ってほしくありません。問題点と、改善したサンプルコードを共有します。
サンプル
まずは、問題がある例をCodePenで再現しました。こちらのリンクからご覧ください。
それを改善した例がこちらです。
…
…
同じですね。見た目は、全く同じです。
先に挙げた例の問題点を説明します。
問題点
1. クリック、タップ以外での操作ができない
「CSSだけで作るタブメニュー」の仕組みですが、タブメニューのボタンに隠してあるラジオボタンと、表示したいコンテンツをCSSの「後続兄弟結合子」で関連付けることによって実現しています。
#type01-all:checked ~ #type01-all__content,
#type01-programming:checked ~ #type01-programming__content,
#type01-design:checked ~ #type01-design__content {
display: block;
}
さらにタブメニューのボタンをlabel要素 にして、for属性で結びつけることで、クリック可能としています。
<input id="type01-all" type="radio" name="tab_type01" checked>
<label class="tabs__item" for="type01-all">アクセシビリティ</label>
これには大きな問題はないのですが、検索して見つかる記事はすべて、選択の対象となるラジオボタンを display:none
で隠してしまっています。
.tabs.tabs--type01 input[name*="tab_"] {
display: none;
}
display:none
で隠した要素は、存在しないものとして扱われます。
フォーカスができないため、クリック、タップ以外での操作はできません。
一方で、スクリーンリーダーからコンテンツを隠すので、visibility:hidden や display:none は使用しないでください。 正当な理由があるのでなければ、なぜこのコンテンツをスクリーンリーダーから隠したいのでしょうか。
2. スクリーンリーダーの読み上げ対象にならない
存在しないものとして扱われる、ということは、スクリーンリーダーでも対象外となります。ボタンのテキストは読み上げられますが、そこに切り替えのインターフェイスがあることは、スクリーンリーダーの利用者にはわかりません。
3. タブとコンテンツに関連性がない
これは細かい話ですが、問題がある例では、タブとコンテンツには文書上の関連性がありません。見出しが3つ並んだリストと、見出しがないコンテンツを3つ並べたセクション群でしかありません。
改善案
これらの問題を、ラジオボタンと後続兄弟結合子を使うというルールを外さずに改善を試みたのが、先に挙げた改善案です。
1. ラジオボタンとコンテンツを position で隠す
position: absolute
で隠した要素は、ブラウザ上で対象外となりません。音声読み上げでも正しく読み上げられます。デジタル庁のサイトでも採用されている手法で隠しています。
.tabs.tabs--type02 input {
border: 0 !important;
clip: rect( 0, 0, 0, 0 ) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
white-space: nowrap !important;
width: 1px !important;
}
さらに、タブコンテンツの方も、表示しなくても認識されるよう position: absolute
に書き換えています。もしもJavaScriptを使用していて、 aria-hidden
属性を付与していた場合は、コンテンツにはそこまでしなくてもよいかもしれませんが…
2. WAI-ARIAを使用する
改善したタブメニューは、aria属性による最適化をしています。明確にタブをグループ化し aria-label
属性を付与し、これがタブメニューであることを明示しています。
aria-label
属性は、要素が role
属性を持つことが前提なので role="tab"
も付与しています。
<div class="tabs__list" role="tab" aria-label="タブメニュー:アクセシビリティについて">
<label id="type02-all" class="tabs__item" for="type02-all-input">
<input id="type02-all-input" type="radio" name="tab_type02" checked>
アクセシビリティ
</label>
・・・以下略
さらにコンテンツ側には aria-labelledby
属性を付与しています。この属性は、コンテンツの見出しに該当する要素をIDで指定します。下記の、一つ目のコンテンツは type02-all
ですから、タブボタンの「アクセシビリティ」が見出しである、ということになります。
こちらにも role
属性を付与しています。
<div class="tabs__content" id="type02-all__content" role="tabpanel" aria-labelledby="type02-all">
・・・以下略
タブメニューをまとめたことで、HTMLのツリーが一階層下がり、セレクタも再考が必要になりました。 :has()
を使用することで同様の挙動を実装しています。
セレクタがIDに依存してしまっていることが気に入らない
.tabs.tabs--type02 .tabs__list:has(#type02-all > input:checked) ~ #type02-all__content,
.tabs.tabs--type02 .tabs__list:has(#type02-programming > input:checked) ~ #type02-programming__content,
.tabs.tabs--type02 .tabs__list:has(#type02-design > input:checked) ~ #type02-design__content {
overflow: visible;
position: static;
height: auto;
padding: 2rem;
}
3. フォーカスに対応
問題がある例は、キーボードでフォーカスしたときを考慮していません。このため、フォーカスしたときも同じスタイルが適用されるようにしています。ラジオボタンにチェックが入っていて、さらにフォーカスもしている状態にも対応しなければなりません。
フォーカスしたときだけ、アウトラインリングが出るようにしたい…という場合は、CSSはより複雑になると思います。
.tabs.tabs--type02 .tabs__item:hover,
.tabs.tabs--type02 .tabs__item:has(input:focus) {
background-color: #dddddd;
}
・・・中略
.tabs.tabs--type02 .tabs__item:has(input:checked),
.tabs.tabs--type02 .tabs__item:has(input:focus:checked) {
background-color: #3399CC;
color: #fff;
}
おわりに
問題点はどれもアクセシビリティに関することです。時間がないんだ、そこまで考慮しなくていい…というのであれば個人の自由ですが、最近はマウスで操作できない人=障害を持つ人とは限りません。
ゲームサイトであればコントローラやVRデバイスで操作している可能性もありますし、マウスが電池切れで使えない…!などのケースもあるでしょう。効率化のために音声読み上げを使用している人もいます。令和のコーダーであれば、最低限のアクセシビリティ対応は常識として捉えるべきです。
ここまで書いておいてなんですが、厳密にタブメニューを設計するなら、やはりJavaScriptを併用した方が良いです。タブコンテンツに aria-hidden
属性を使用できます。
私の案は完全でないと思います。ぜひ改良をお願いいたします。