🪝

Inline stylesで擬似クラスが書ける! CSS Hooksが面白い

2023/12/25に公開

今回は、最近特に英語圏で注目を集めているCSSライブラリ「CSS Hooks」についてご紹介します!
このライブラリは、CSS Modulesのco-authorでvanilla-extractの作者であるMark Dalgleish氏が大注目しており、自分はこのポストが知るきっかけになりました。

https://twitter.com/markdalgleish/status/1729399475494608923

日本語での情報が少なそうだったので、基本的なライブラリの紹介、使い方、仕組みについて触れていこうと思います。

CSS Hooksとは

https://css-hooks.com/

Inline styles形式で型安全にCSSを記述できるライブラリです。
通常のInline stylesと異なり、擬似クラスやメディアクエリ、コンテナクエリなども同様にInline styles形式で書くことができるというところが特筆すべき点です!これは素のInline stylesにはできない革新的な機能です。

これによってInline stylesの可能性は大きく広がります。
例えば、Inline stylesはHTMLマークアップに直で埋め込まれているため、通常バンドル時に行うようなCSSまわりのビルドステップは不要になります。また、同じ理由でサーバーサイドレンダリング時も問題なく動きます。他のCSSライブラリだと、SSR用にプラスアルファで設定が必要になることが多いですが、このライブラリでは不要になります。シンプルでいいですね。

基本的な使い方

Getting startedをみながら進めていきます。

https://css-hooks.com/docs/react/getting-started

今回の例ではReactを使用します(公式Docsをみると、SolidJSやPreactもサポートしていそうですね)。

まずパッケージをインストールしたら、以下のように"Hooks"を定義します。
自分が使う分の擬似クラス、メディアクエリなどを定義することであとで吐き出されるCSSが減ります。

// src/css.ts
import { createHooks } from "@css-hooks/react";
    
export const [hooks, css] = createHooks({
  "&:hover": "&:hover",
});

TypeScript5.3以上の場合は以下のように書くこともできます。

import { createHooks } from "@css-hooks/react";
import { recommended } from "@css-hooks/recommended";

export const [hooks, css] = createHooks(
  recommended({
    pseudoClasses: [":hover"],
  })
);

次にこのHooksを、アプリのrootでstyleタグに埋め込みます。
これだけで準備は終わりです!

// src/App.tsx
import { hooks, css } from "./css";

function App() {
  return (
    <>
      <style dangerouslySetInnerHTML={{ __html: hooks }} />
      <button
        style={css({
          backgroundColor: "gray",
         "&:hover": {
            backgroundColor: "red",
          },
        })}
      >
        Hover!
      </button>
    </>
  );
}

要素をhoverすると背景色が変わっていることが確認できますね!

CSS Hooksが擬似クラスなどを扱える仕組み

ここまで説明してきたように、CSS Hooksを使うとシンプルに擬似クラス、メディアクエリなどを書くことができますが、果たしてどういう仕組みになのでしょうか。

結論としては、CSS HooksはCSS Variablesを上手に活用しています。
具体的にはCSS Variablesの第二引数です。

例えば以下のようなCSSを書いたとします。

.hoge {
  color: var(--primary, blue)
}

このコードが意味するのは、--primaryがvalidな場合は--primaryを適用し、そうでない場合はblueにfallbackさせるということです。

CSS Hooksはこの仕組みを利用しています。

「基本的な使い方」で紹介した例に戻って考えてみる

こちらが先ほどの例で最終的に主力されるコードになります。

<style>
  * {
    --mbscpo-0: initial;
    --mbscpo-1: ;
  }
  *:hover {
    --mbscpo-0: ;
    --mbscpo-1: initial;
  }
</style>
<button style="background-color: var(--mbscpo-1, red) var(--mbscpo-0, gray)">
  Hover!
</button>

まずは通常時の以下styleに注目してみます。

* {
    --mbscpo-0: initial;
    --mbscpo-1: ;
  }

--mbscpo-0というクラスに対して、initialという値が当たっています。
var()関数で第一引数がinitialの場合、第二引数にfallbackされるという仕組みなので、
var(--mbscpo-0, gray)はgrayにfallbackされ、ボタンはグレーになります

一方、--mbscpo-1: ;とあるように、--mbscpo-1には空の値があたっています。この場合、var(--mbscpo-1, red)に関してはfallbackはされず空の値が優先されるため、結果としてボタンに赤色は適用されません

hover時は、逆に以下のスタイルが有効になるので、赤色が適用されるようになります。

*:hover {
  --mbscpo-0: ;
  --mbscpo-1: initial;
}

このようにしてCSS Hooksはスタイルの条件付きトグルを実現しているのですね!🙌

ライブラリ作者のNick Saunders氏のブログでさらに詳しく解説されています↓

https://nsaunders.dev/posts/css-madness-to-hooks

CSS Hooks(Inline styles)の懸念点

CSS Hooksはその画期的なアイデアで注目を浴びている一方で、Inline stylesの使用に関して、X上ではいくつかネガティブな指摘が見受けられます。
とりわけ問題視されていそうなのは、パフォーマンスについてです。
Inline Stylesは、Reactの旧公式ドキュメントGoogleのPageSpeed Insightsのガイドラインで、コードの重複に繋がるなどの理由で使用が推奨されていないので、一般的には避けるのがベターなようです。

これに対して、Nick Saunders氏は、Inline stylesがatomicなCSSよりもパフォーマンスが劣るとはいえ、それほど悪くないと考えていそうです。主張の根拠として、HTMLのパースからペイントまでの時間を測定した結果、Inline stylesはatomicなCSSよりもわずかに30%ほどしか遅くならなかったという記事を参照しています。

https://twitter.com/agilecoder/status/1732450509582123292

これに関しては賛否両論あるかもしれませんが、ブラウザ側がInline stylesのペイントをさらに最適化してくれるような未来があれば、また変わった議論になってくるかもしれないですね!

CSS Hooksのその他の懸念点、改善点については以下のissueにまとまっているので、気になった方は是非みてみてください!

https://github.com/css-hooks/css-hooks/issues/27

おわりに

今回はCSS Hooksというライブラリをご紹介しました。
このライブラリを含め、たくさんのCSSソリューションが存在する現代のモダンWebフロントエンドが今後どうなっていくか楽しみですね!
Happy Coding & Happy Holidays!🎅

Discussion