「CSSだけで作るタブメニュー」の重大な問題と改善案

会社の後輩コーダーが、次の案件で「CSSだけで作れるタブメニュー」を使う予定です！と言って、紹介記事を教えてくれました。
そのサンプルコードを見てみましたが、**アクセシビリティ面に重大な問題**があります。その記事だけではなく、「CSS タブ」などで検索して1ページ目に出てくる記事は、すべて同様の手法を使っていました。

私としては、後輩にも、令和のすべてのコーダーにもこの手法を使ってほしくありません。問題点と、改善したサンプルコードを共有します。

## サンプル

まずは、問題がある例をCodePenで再現しました。こちらのリンクからご覧ください。

https://codepen.io/webbingstudio/pen/pvzdaWZ

![スクリーンショット 2025-01-04 17.23.43.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21117/1f625a88-4ee3-910e-44ea-e3da482d633e.jpeg)

それを改善した例がこちらです。

https://codepen.io/webbingstudio/pen/vEBWdWX

![スクリーンショット 2025-01-04 17.26.05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21117/9869c807-8635-064e-8854-5fa55427b597.jpeg)

…

…

同じですね。見た目は、全く同じです。
先に挙げた例の問題点を説明します。

## 問題点

### 1. クリック、タップ以外での操作ができない

「CSSだけで作るタブメニュー」の仕組みですが、タブメニューのボタンに隠してあるラジオボタンと、表示したいコンテンツをCSSの「後続兄弟結合子」で関連付けることによって実現しています。

https://developer.mozilla.org/ja/docs/Web/CSS/Subsequent-sibling_combinator

``` CSS
#type01-all:checked ~ #type01-all__content,
#type01-programming:checked ~ #type01-programming__content,
#type01-design:checked ~ #type01-design__content {
  display: block;
}
```

さらにタブメニューのボタンをlabel要素 にして、for属性で結びつけることで、クリック可能としています。

``` html
<input id="type01-all" type="radio" name="tab_type01" checked>
<label class="tabs__item" for="type01-all">アクセシビリティ</label>
```

これには大きな問題はないのですが、検索して見つかる記事はすべて、選択の対象となるラジオボタンを `display:none` で隠してしまっています。

``` CSS
.tabs.tabs--type01 input[name*="tab_"] {
  display: none;
}
```

**`display:none` で隠した要素は、存在しないものとして扱われます。**

フォーカスができないため、クリック、タップ以外での操作はできません。

https://developer.mozilla.org/ja/docs/Learn_web_development/Core/Accessibility/Tooling#%E3%82%88%E3%81%8F%E3%81%82%E3%82%8B%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B7%E3%83%93%E3%83%AA%E3%83%86%E3%82%A3%E3%81%AE%E5%95%8F%E9%A1%8C

> 一方で、スクリーンリーダーからコンテンツを隠すので、visibility:hidden や display:none は使用しないでください。 正当な理由があるのでなければ、なぜこのコンテンツをスクリーンリーダーから隠したいのでしょうか。

### 2. スクリーンリーダーの読み上げ対象にならない

存在しないものとして扱われる、ということは、スクリーンリーダーでも対象外となります。ボタンのテキストは読み上げられますが、そこに切り替えのインターフェイスがあることは、スクリーンリーダーの利用者にはわかりません。

### 3. タブとコンテンツに関連性がない

これは細かい話ですが、問題がある例では、タブとコンテンツには文書上の関連性がありません。見出しが３つ並んだリストと、見出しがないコンテンツを３つ並べたセクション群でしかありません。


## 改善案

これらの問題を、ラジオボタンと後続兄弟結合子を使うというルールを外さずに改善を試みたのが、先に挙げた改善案です。

https://codepen.io/webbingstudio/pen/vEBWdWX

### 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` 属性を使用できます。

私の案は完全でないと思います。ぜひ改良をお願いいたします。