CSS 変数のフォールバックトリックを使った CSS Hooks というスタイリング用ライブラリの仕組み

## この記事の概要

CSS Hooks という、インラインスタイルの記述で擬似要素やブレイクポイントまで設定できるライブラリがあります。

https://css-hooks.com/

どういう仕組みか気になったので、調べてみました。

:::note warn
このライブラリの使い方自体はシンプルなので、この記事で解説はしません。
:::

## CSS 変数のフォールバックトリック

ドキュメントにあるように、CSS 変数のフォールバックトリックを使って実現されています。

https://css-hooks.com/docs/react/introduction#top

と言っても、これを読んだだけではイマイチ分からなかったので、もう少し詳細に書きます。
上記ドキュメントでも例で使われているコードをベースに説明します。

```html
<a href="#" style="color: var(--hover-on, #f00) var(--hover-off, #0f0);">
  Hover me
</a>
<style>
  * {
    --hover-off: initial;
    --hover-on: ;
  }
  :hover {
    --hover-off: ;
    --hover-on: initial;
  }
</style>
```

### マウスカーソルを乗せていないときのスタイル

`*` では `--hover-on: ;` が定義されています。
CSS 変数において、空文字列は有効な値であるため、`var(--hover-on, #f00)` は空文字列として処理されます。

つまり、先ほどのコードは以下のように解釈されます。

```html
<a href style="color:  var(--hover-off, #0f0);">
```

そして今度は、`--hover-off: initial;` に着目します。
`initial` は [guaranteed-invalid value](https://www.w3.org/TR/css-variables/#guaranteed-invalid) として定義されているので、フォールバックで指定した `#0f0` が有効になります。

### マウスカーソルを乗せているときのスタイル

`:hover` では `--hover-off: ;` が定義されています。
先ほどと同様、空文字列として処理されて、コードは以下のように解釈されます。

```html
<a href style="color: var(--hover-on, #f00) ;">
```

そして `--hover-on: initial;` が定義されています。
そのため `var(--hover-on, #f00)` の部分で、フォールバックで指定した `#f00` が有効になります。

## フォールバックトリックの入れ子

CSS 変数を入れ子にすれば、例えばメディアクエリと擬似クラスを併用できます。
その際のコードは以下のようなイメージになります。


```html
<a href style="
  color:
    var(--hover-on,
      var(--desktop-on, #00f)
      var(--desktop-off, #0f0)
      ) 
    var(--hover-off, #f00);"
>
  Hover me
</a>
<style>
  * {
    --desktop-off: initial;
    --desktop-on: ;
    --hover-off: initial;
    --hover-on: ;
  }
  @media (640px <= width) {
    * {
      --desktop-off: ;
      --desktop-on: initial;
    }
  }
  *:hover {
    --hover-off: ;
    --hover-on: initial;
  }
</style>
```

## CSS Hooks が何を担っているか

前のセクションに記載したように、条件が増えれば増えるほど記載の仕方が複雑になります。
また、CSS 変数の名前の管理も大変ですし、補完も効きません。

このような問題を解消するために、CSS Hooksが設定ファイルから自動で上手く処理してくれるようになっています。

ドキュメントにあるように設定すると、

```typescript:css.ts
import { createHooks } from "@css-hooks/react";
import { recommended } from "@css-hooks/recommended";

export const [hooks, css] = createHooks(recommended({
  breakpoints: ["500px", "1000px"],
  colorSchemes: ["dark", "light"],
  pseudoClasses: [
    ":hover",
    ":focus",
    ":active",
    ":disabled",
  ]
}));

/* Hooks created:
 - @media (width < 500px)
 - @media (500px <= width < 1000px)
 - @media (1000px <= width)
 - @media (prefers-color-scheme: dark)
 - @media (prefers-color-scheme: light)
 - &:hover
 - &:focus
 - &:disabled
 - &:active
*/
```

このような CSS 変数が出力されます。

```css
* {
  --i73cho-0: initial;
  --i73cho-1: ;
  --acsdpe-0: initial;
  --acsdpe-1: ;
  --1a96iy-0: initial;
  --1a96iy-1: ;
  --iosurw-0: initial;
  --iosurw-1: ;
  --aio4p7-0: initial;
  --aio4p7-1: ;
  --mbscpo-0: initial;
  --mbscpo-1: ;
  --lddb28-0: initial;
  --lddb28-1: ;
  --fg65d2-0: initial;
  --fg65d2-1: ;
  --u6v7g0-0: initial;
  --u6v7g0-1: ;
}
@media (prefers-color-scheme: dark) {
  * {
    --i73cho-0: ;
    --i73cho-1: initial;
  }
}
@media (prefers-color-scheme: light) {
  * {
    --acsdpe-0: ;
    --acsdpe-1: initial;
  }
}
@media (width < 500px) {
  * {
    --1a96iy-0: ;
    --1a96iy-1: initial;
  }
}
@media (500px <= width < 1000px) {
  * {
    --iosurw-0: ;
    --iosurw-1: initial;
  }
}
@media (1000px <= width) {
  * {
    --aio4p7-0: ;
    --aio4p7-1: initial;
  }
}
*:hover {
  --mbscpo-0: ;
  --mbscpo-1: initial;
}
*:focus {
  --lddb28-0: ;
  --lddb28-1: initial;
}
*:active {
  --fg65d2-0: ;
  --fg65d2-1: initial;
}
*:disabled {
  --u6v7g0-0: ;
  --u6v7g0-1: initial;
}
```

名前被りが起こらないようになるのと、補完や型安全性なども得られます。

## 最後に

Tailwind が流行りはじめた際「じゃあインラインスタイルで良いじゃん」「いやそれだと擬似要素やメディアクエリが使えない」というやり取りをよく見ました。

まさかこんな解決策があるとは……。

とは言え Atomic class として出力されるよりは CSS としてのコード量が増えそうですし、絶対に CSS Hooks を使いたいかというと微妙なところです。

まだ生まれて間もないライブラリでもありますし、今後の動向をチェックしておこうかな、くらいの気持ちです。