アクセシビリティを意識した tailwind modifier のすすめ

この記事は[LIFULL Advent Calendar 2022](https://qiita.com/advent-calendar/2022/lifull)の14日目です。

フロントエンドエンジニアのえびです。好きな寿司ネタはえんがわです🍣
普段は LIFULL HOME'S の賃貸領域の開発を行っています。

最近、実業務で tailwindcss を使うことが増えました。
命名やCSSファイルとの往復もせずに HTML にガリガリスタイルを当てていけるので非常に効率が良いのですが、状態に応じたスタイルを当てるのにやや苦労していました。

しかし、 アクセシビリティ（a11y） を意識すると快適なスタイリング・堅牢なマークアップが両立できます。
この記事では状態に応じたスタイリング手法を紹介しつつ、アクセシビリティを意識した tailwind modifier をご紹介できればと思います。

## 状態に応じたスタイリング(modifier)

コーディングを進める上で、インタラクティブな要素には状態に応じたスタイリングが求められます。
たとえばボタンの場合、`hover/active/focus` などが挙げられるでしょうか。いわゆる擬似クラスです。

tailwind でこれらのスタイリングを行う場合、 [modifier](https://tailwindcss.com/docs/hover-focus-and-other-states) を使います。
button に対して `hover:bg-..., active:bg-... focus:ring..` などのように `[status]:[class]` の形式で書くことによってその状態のスタイルを当てられます。

また、論理属性に対しても modifier は扱えます。
よく使うものでいうと input の `checked:...` が挙げられるでしょうか。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="WNyBEap" data-user="ebisawa-next" 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/ebisawa-next/pen/WNyBEap">
  Untitled</a> by Moeko Ebisawa (<a href="https://codepen.io/ebisawa-next">@ebisawa-next</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

他にも odd/even や nth-child, only-child など順番に関わるものも標準対応されています。

## 通常の modifier では解決できないスタイリング

ただし、現代のUIはこれだけだと完全なスタイリングは行えません。
たとえばアコーディオンを details/summary を使わずに自力で実装する場合、「要素が開いている時はこのスタイルを当てたい」という場合に擬似クラス/論理属性では表現することができませんでした。
そのため、別途定義しておいた css を js 側でつけ外しをして要素の開閉を表現する必要がありました。場合によっては子要素のスタイルも丁寧につけ外しをする必要があります😇
これでは tailwind の旨味は薄れてしまいます。

しかし10月ごろに発表された v3.2 で実装された新機能により状況が大きく変わりました。
modifier に関わる3つを紹介していきます。

### 1. Dynamic group-* and peer-* variants

先の項で申し上げたとおり、tailwind はその性質上従来の CSS のように `is-hoge` `hoge--mofifier` のように親/兄要素に状態変化クラスをつけて子/弟要素でスタイリングすることが出来ませんでした。
しかし、 v3.2 で実装された [Dynamic group-* and peer-variants](https://tailwindcss.com/blog/tailwindcss-v3-2#dynamic-group-and-peer-variants) によってこれらを group/peer でも使えるようになりました。
あるクラスを親/兄要素が持っている場合、 **子/弟要素にも影響させることができます。**
以前から modifier として擬似クラス `.parent:hover .children` `.parent:active + .children` などは対応していましたが、
`.parent.is-hoge .children` `.parent.is-hoge + .children` もいけるようになったのです。
親/兄には状態変化用のクラス `is-hoge` を当て、子/弟には `(group|peer)-[.is-hoge]:クラス名` のように書けばOKです。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="qBKoMLB" data-user="ebisawa-next" 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/ebisawa-next/pen/qBKoMLB">
  Untitled</a> by Moeko Ebisawa (<a href="https://codepen.io/ebisawa-next">@ebisawa-next</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

ただしこのスタイリング方法には欠点があり、**状態変化クラスを使って親/兄要素自体へのスタイリングが行えません。**
そのため、親/兄要素自体のスタイルを変えたい場合は別途 css を書く必要があります😇
これでは tailwind の旨味は享受しにくいですね。swiper などのライブラリで別途スタイルを当てる時くらいしか用途が思い浮かびません。。
基本的にはこれを使うより、次で紹介する Data Attribute Variants を使っていくのが無難でしょう。

### 2. Data attribute variants

[Data attribute variants](https://tailwindcss.com/blog/tailwindcss-v3-2#data-attribute-variants) は、data属性に応じてスタイルを変えられます。
以前から plugin で書けば同様のことはできましたが、v3.2 で arbitrary value チックに書けるようになり気軽に使えるようになりました。
これは modifier として扱えるため親/兄弟要素にもスタイルをアタッチできますし、子/弟要素に group/peer でスタイルを伝播させることも可能です。ぶっちゃけ Dynamic group-* and peer-* variants の上位互換と言えるでしょう。

書き方は `data-[name=value]` の形式で、 group/peer で扱う場合は `(group|peer)-data-[name=value]` と書きます。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="VwdOzBJ" data-user="ebisawa-next" 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/ebisawa-next/pen/VwdOzBJ">
  Untitled</a> by Moeko Ebisawa (<a href="https://codepen.io/ebisawa-next">@ebisawa-next</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### 3. ARIA attribute variants

[ARIA attribute variants](https://tailwindcss.com/blog/tailwindcss-v3-2#aria-attribute-variants) = **ARIA modifier の標準対応** です。

v3.2 以前から ARIA modifier を使う術はありました。
有志によって開発された [tailwindcss-aria-attributes](https://github.com/thoughtbot/tailwindcss-aria-attributes) という package を使ったり `tailwind.config.js` で `addVariants` することで設定できました。
自分もちまちま設定を書いていたのですが、この度公式で設定しやすい環境になりました☺️

この ARIA attribute variants は modifier になりますので、Data Attribute Variants と同様に親/兄要素自体へのスタイリングも可能です。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="GRGxeQL" data-user="ebisawa-next" 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/ebisawa-next/pen/GRGxeQL">
  Untitled</a> by Moeko Ebisawa (<a href="https://codepen.io/ebisawa-next">@ebisawa-next</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

v3.2 現在標準対応しているのは以下の8属性ですが、

| Modifier | CSS |
| ---- | ---- |
| aria-checked | &[aria-checked=“true”] |
| aria-disabled | &[aria-disabled=“true”] |
| aria-expanded | &[aria-expanded=“true”] |
| aria-hidden | &[aria-hidden=“true”] |
| aria-pressed | &[aria-pressed=“true”] |
| aria-readonly | &[aria-readonly=“true”] |
| aria-required | &[aria-required=“true”] |
| aria-selected | &[aria-selected=“true”] |

`tailwind.config.js` に `aria` を追記することで簡単に設定を増やせます。 `addVariants` よりお手軽ですね。

```js:tailwind.config.js
module.exports = {
  theme: {
    extend: {
      aria: {
        invalid: 'invalid="true"',
      },
    },
  },
};
```

```tsx:input.html
// 白い背景のテキストフォーム
<input type="text" value="hoge" class="bg-white aria-invalid:bg-red" />

// 赤い背景のテキストフォーム
<input type="text" value="hoge" class="bg-white aria-invalid:bg-red" aria-invalid="true" />
```

## Data attribute variants と ARIA attribute variants の使い分け

data 属性でスタイルを変えられるなら今までのように js で data 属性をアタッチしてスタイルをハンドリングしてもいいんじゃない？と思われる方も多いと思います。
しかし、ARIA 属性を扱える場面では ARIA attribute variants を使う方が堅牢でメンテナビリティの高いコードになります。

### ARIA attribute variants を使った方がよい場面

例えば先の項目で提示したタブパネルですが、これらは [Tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) という WAI-ARIA で提示されている tab/tabpanel role が持つべきふるまいを実装しています。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="GRGxeQL" data-user="ebisawa-next" 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/ebisawa-next/pen/GRGxeQL">
  Untitled</a> by Moeko Ebisawa (<a href="https://codepen.io/ebisawa-next">@ebisawa-next</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

タブが選択された際に `aria-selected` という属性を `true` にする必要があるため、おのずと js もそのような実装になります。

```js
  /**
   * タブにフォーカスします
   * @param target - tabTarget
   */
  focusTab(target) {
    target.setAttribute('tabindex', '0');
    target.setAttribute('aria-selected', 'true');
    target.focus();
  }

  blurAllTabs() {
    this.tabTargets.forEach((target) => {
      target.setAttribute('tabindex', '-1');
      target.setAttribute('aria-selected', 'false');
    });
  }
```

そしてタブが選択されている場合はタブの背景色を変えたいため、html 側はこんな感じのスタイリングをすることになります。
※わかりやすさのために Stimulus controller アタッチ周りは省略

```html
      <button
        class="p-2 rounded-t-md border transition-colors hover:bg-yellow-100 aria-selected:bg-yellow-100"
        role="tab"
        aria-selected="true"
        aria-controls="panel-1"
        id="tab-1"
        tabindex="0"
       >
        First Tab
      </button>
```

`aria-selected:` という直感的でわかりやすい modifier を扱える上に、アクセシビリティを担保した WAI-ARIA に準じた実装を行えるのでかなり堅牢なマークアップになります。
これが data 属性の取り外しだけでスタイリングをやっていると js 側で data 属性をいじっただけで実装が崩壊しかねませんし、なんのための data 属性なのかを js 側で考える必要が出てしまいます。

### Data attribute variants を使った方が良い場面

ARIA 属性はそれっぽい状況になったら絶対に使えるというわけではありません。
正しい用法を理解していないとかえってユーザーの妨げに繋がり、アクセシビリティを損ねてしまうことに注意が必要です。

[ARIA Authoring Practices Guide (APG) の Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/) を熟読した上で「このモジュールはこのデザインパターンに合致するな」と思ったら取り入れていくのが良いでしょう。
そうでないものの場合は Data Attribute Variants を使うのが無難です。

たとえば input 要素自体にエラーが出ているときは `aria-invalid:~` を使えば良いですが、
フォームを囲っている `fieldset` や `div` に対してスタイルを当てたいぞ！というときは Data Attribute Variants で `data-[error="true"]:~` のようにスタイリングするのがよいでしょう🙆‍♂️

## まとめ

- 状態に応じたスタイリングするなら以下の3種類を使うとよい
  - 擬似クラス modifier: `[status]:...`
  - Data Attribute Variants: `data-[name=value]:...`
  - Aria Attribute Variants: `aria-[status]:...`
- 基本は擬似クラスの modifier を使おう
- WAI-ARIA に則ったデザインパターンの場合は Aria Attribute Variants を使おう
- それ以外の場合は Data Attribute Variants を使おう

この記事がよりよいコーディングライフの一助になれば幸いです🥳