🧊

2023年のCSSは0ランタイムにしたい

したいよね?

事の経緯としては、

  • とある新規プロジェクトで、技術選定をすることに
  • 開発の土台はViteで、フレームワークはJSXが使えるやつに決まった
  • さてCSSはどうやって書こうかとなる
  • あれこれ考えて、とある方法に決めた

というのをまとめた、まあポエムの域を出ないメモです。

CSS ModulesやらTailwindやらCSS-in-JSやら手法はいろいろあれど、どれが今の推しかっていう。

求めるもの

個人的に、CSSつまりはブラウザで表示されるUIをスタイリングするためのツールセットに求めるもの。

  • 0ランタイムである
    • 最終的に`.css`ファイルとしてブラウザで読み込まれる
    • なんでもJSにしない
  • CSSの書き味を損なわない
  • マークアップ部と1ファイルにコロケーションできる
    • HTMLとCSSは同居させたい
  • 非同期にロードできる
    • JSと同じく、いま表示していないUIに関するCSSは読み込まない
  • Tree shakingできる
    • JSと同じく、無駄なものをバンドルしない
  • DXを損なわない
    • エディタのサポートなど選択肢が広い、ニッチでない

こうやって見るとハードル高い?って自分でも思うけど、理想として求めるラインという意味ではこんなもん。

で、これをできる限り満たすソリューションを求めた。

落選: Tailwind

なんか流行ってるし、輝くシーンもきっとあるとは思うけど、未だにその場面に出会ったことはない。

落選: CSS Modules

`.css`ファイルなのでエディタまわりの心配もないし、ツールのセットアップや依存の少なさという意味でもかなり優秀な択。

ただ、DXまわりにはやや懸念があるなという印象。

たとえばクラス名を`class={styles.xxx}`って入力するときに、LSPから補完すらされないので、別途ツールが必要とか。

GitHub - mrmckeb/typescript-plugin-css-modules: A TypeScript language service plugin providing support for CSS Modules.

ただTypeScriptのプラグインコンパイルに介入できないので、突き詰めるとCLIツールを常駐させないと不便だとか。

先駆者たちがこのあたりは熱心に取り組んではいるものの、どれも一長一短だな〜と。

コードジャンプ可能な CSS Modules を実現する happy-css-modules の紹介 - mizdra's blog

個人的にはクラス名の補完さえあれば満足できるけど、プロジェクトの規模によっては欲しくなるよなというのもわかる。

なので、コロケーションの問題さえ飲めれば手堅い選択肢・・・やったけど、いつの時代もあと一歩が惜しい位置にいるイメージ。

落選: Svelte(/ Astro)

今回は落選。というのも、フレームワークは別で決まっていたから・・・!

そうでなければ、求める要件をすべてクリアしてるので、別の機会では間違いなく選出される。

1つ気になるとすれば、`.svelte`ファイルは独自のCSSコンパイラを通すので、たとえば`@container`みたいな最新の記法に未対応だったりする。

CSS container queries "@container" breaks parser · Issue #6969 · sveltejs/svelte · GitHub

まあこれは時間の問題ではあるけど。

ただこのコンパイラのおかげで、そのテンプレにおいて存在しないセレクタを書いただけで`Unused CSS selector`って警告してくれたりと手厚い。

もちろんそれを支えるツール群(`svelte-check`とかLSP)は必要になるものの、Svelteやるならもう揃ってるという敷居の低さがさすがやなって感じ。

当選: CSS-in-JS

そしてやはりJSエコシステムに帰ってくるのか〜〜って我ながら思った。

が、2023年のCSS-in-JSは一味違ってて。
有名どころだと`vanilla-extract`とか`linaria`とか、あの頃にはなかった0ランタイムCSSできる選択肢が増えてる。

最終的に`.css`ファイルになるなら、別にJSで書いても何の気負いもないし、JSなので非同期ロードもTree-shakingもなんでもござれ、コロケーションももちろん可能という夢の選択肢に見える。
0ランタイムにするために、今までのように`props`から動的な定義ができないとか特別な記法が必要とかはあるものの、なんとでもなる範囲かと。

で、このCSS-in−JSにはいくつか書き方があって、その中のどれでやるかを選ぶ必要がある。(ライブラリによって対応してたりしてなかったりする)

  • いわゆるstyled-componentsのスタイル
  • Scopedなクラス名だけ使うスタイル

前者は`styled.div()`みたいな書き方で、装飾つきDOM要素をコンポーネントとして定義するやつ。

const Title = styled.h1`
  font-family: "Noto Sans";
`;

こういうやつ。
ただこっちはそのフレームワーク用のインテグレーションが必要だったり、`ref`の取り回しに毎回苦労したりするので、個人的には推してない。

つまりは後者のクラス名だけ扱えるスタイルでよいけど、こっちはさらに記法が2パターンある。

// 1. オブジェクト
const myButton1 = css({
  color: "orange",
  fontSize: "13px",
});

// 2. テンプレートリテラル
const myButton2 = css`
  color: orange;
  font-size: 13px;
`;

ここは少し迷った。

オブジェクトの場合

こっちのメリットは、完全にJavaScriptであるがゆえDXを助けるツールなどが一切必要ないこと。

シンタックスハイライトも、LSPの補完も、JS(TS)を書くための仕組みがあればそれで揃う。シュッとしてる!

デメリットとしては、やはりCSSっぽさが薄れること。キャメルケースで書くだけと思いきや、メンタルモデルとしてやはり引っかかるものがある。
`Record<string, string>`な型に対していまいち安心感がないのに似てる。中途半端っていうか。

あとは、0ランタイムでこの記法ができるライブラリが世にあまりないこと。自分の調べでは、1つだけ。

`macaron`(powered by `vanilla-extract`らしい)は割と多機能で便利そうではありつつ、ちょっと身に余るな〜という感じと、記法のクセが許容できなかった。

まず、`&`や`[data-xxx]`みたいな自身を対象とする特殊なセレクタを書くために、`selectors`という層が必要なところ。

const linkStyle = style({
  color: "red",
  ":hover": {
    color: "tomato",
  },
  // コレ
  selectors: {
    "&.-active" {
      color: "tomato",
    },
  },
});

また、そのスタイル定義において、自身ではないセレクタを対象に取れないところ。

const containerStyle = style({
  display: "flex",
  
  // ここで子に対する指定を書きたいが書けない
  // "> main": {
  //   flex: "1 1 auto";
  // }
});

// 書けないので、別のクラスを用意して割り当てる必要がある
const itemStyle = style({
  flex: "1 1 auto";
});

というあたりがCSSっぽい書き味から遠くて不便だった。
コンポーネントを小さくしてScopedでCSSを書く一番のメリットは、クラス名をいちいち振らなくても雑にスタイリングできるところだと思ってるので。

あとはクラス定義が増えると、テンプレに書かれるクラス名も増えることになり、JS(JSX)のサイズにも多少影響するのが気になる。

テンプレートリテラルの場合

というわけで、最終的にたどり着いたのがこの方法だった。

こっちの記法が0ランタイムで使えるライブラリとしては、この2つを見つけた。

`linaria`は0ランタイム界隈の先駆けみたいなとこもあるし、割と有名なはず。
`ecsstatic`は後発でViteのみ対応だが、小さくて必要最低限な感じが自分好みどストライクって感じ。(今回採用したのはコレ)

この方式は、最初に書いた求める要件はほぼクリアしているものの、DXまわりだけ少し懸念あり(VSCodeを使わない場合)という選択肢だった。

VSCodeを使う場合は、この拡張さえあればすべてうまくいく。

vscode-styled-components - Visual Studio Marketplace

当方NeoVimユーザーなので、`nvim-treesitter`によるハイライトと、LSP補完のためにTypeScriptの拡張を導入しないといけなかった。

GitHub - styled-components/typescript-styled-plugin: TypeScript server plugin that adds intellisense to styled component css strings

あとはエディタに関係なく、CSS文字列に対するチェック(typoとか、CSS定義の不備とか)はビルド時や型チェック時に行うことはできないので、そこは妥協点といった感じ。

Allow "Compiler Plugins" · Issue #16607 · microsoft/TypeScript · GitHub

これが問題になるなら、オブジェクト方式にするしかないけど、もともとCSSってそういうもんかっていう話でもあるかと。

というわけで

2023年のCSSは0ランタイムにしたいので、

というのが個人的な手札ですという記事でした。

あとこれはおまけ情報で、CSS-in−JSなライブラリを探してたときに見つけたこのリスト、網羅性がすごかった。

https://github.com/solidjs/solid-styled-components/issues/40#issuecomment-1281513764

Generationの話はあんまり関係ない。