4月23日、開発者のTen Phi氏が「Why I spent years trying to make CSS states predictable」と題した記事を公開した。
氏は数年の研究を経て、CSSの根本的な問題を解決するTastyというツールを開発した。これは単なるCSS-in-JSライブラリではない。CSS特異性とカスケードが引き起こす「ホバー無効ボタン問題」を根本から断つ、全く新しいアプローチだ。実際にCube Cloudの100以上のコンポーネントで実証済みで、GitHub上でソースコードが公開されている。
なぜ今CSS状態管理が問題なのか
React、Vue、Angularといったコンポーネントベース開発が主流となった現在、1つのコンポーネントが複数の状態(ホバー、アクティブ、無効、ダークモード等)を持つケースが激増している。CSS-in-JSやTailwind CSSといったソリューションが普及する中でも、CSS本来の特異性とカスケードの問題は解決されていない。
最もシンプルな例が「ホバー無効ボタン問題」だ:
.btn:hover { background: dodgerblue; }
.btn[disabled] { background: gray; }
この2つのセレクタは、どちらも特異性が(0, 1, 1)である。ボタンがホバー状態かつ無効状態の場合、ブラウザはソース順序に依存する。:hover規則が後にあれば無効ボタンが青くなり、[disabled]規則が後にあればグレーのままだ。
コンパイラによる決定論的な解決
Ten Phi氏のアプローチは既存のCSS-in-JSとは根本的に異なる。Styled ComponentsやEmotionが「JavaScriptでCSSを書く」のに対し、Tastyは「状態の可能性をすべて宣言し、コンパイラが決定論的なセレクタを生成する」。
import { tasty } from '@tenphi/tasty';
const Button = tasty({
as: 'button',
styles: {
fill: {
'': '#primary',
':hover': '#primary-hover',
':active': '#primary-pressed',
'[disabled]': '#surface',
},
},
});
Tastyはこの状態マップを重複できないセレクタにコンパイルする:
/* [disabled]が完全に優先 */
.t0[disabled] { background: var(--surface-color); }
/* :activeは無効時を除外 */
.t0:active:not([disabled]) { background: var(--primary-pressed-color); }
/* :hoverは:activeまたは無効時を除外 */
.t0:hover:not(:active):not([disabled]) { background: var(--primary-hover-color); }
/* デフォルトは上記のいずれかにマッチする時を除外 */
.t0:not(:hover):not(:active):not([disabled]) { background: var(--primary-color); }
これでカスケードが決着をつける余地がなくなる。2つのブランチが同時にマッチすることはない。
複雑な状態交差でこそ真価を発揮
ホバー無効ボタンは理解しやすい例に過ぎない。本当の価値は、状態がより複雑に交差する場面で現れる。
const Panel = tasty({
styles: {
flow: {
'': 'column',
'@media(w >= 768px)': 'row',
},
fill: {
'': '#surface',
'theme=danger & :hover': '#danger-hover',
'@root(schema=dark)': '#surface-dark',
},
padding: {
'': '4x',
'@(sidebar, w < 300px)': '2x',
},
},
});
3つのプロパティがそれぞれ異なる関心事(メディアクエリ、コンテナクエリ、修飾子、ルート状態、擬似クラス)を持っているが、開発者はそれらがどう相互作用するかを考える必要がない。コンパイラがすでに知っているからだ。
氏によると、「コンポーネントの状態を宣言的に表現し、それを決定論的にするために必要なセレクタロジックをコンパイラに任せる」という発想から、この根本的な解決策が生まれた。
数年の開発と実証済みの信頼性
核となるアイデアはシンプルだったが、実際のツールに変えることが困難だった。「シンプルな状態条件で動作する」から「実世界のコンポーネントシステムをサポートできる」まで到達するのに数年と数百の反復が必要だった。
困難だったのは、擬似クラス、属性、修飾子、ルートレベル状態、メディアクエリ、コンテナクエリ、ネストされた複合セレクタ、スタイルの拡張と安全なオーバーライド、型付きAPIといった要素がすべて同時に現れてもシステムが一貫性を保つことだった。
重要なのは、Tastyが単なる実験ではないことだ。開発当初からCube UI Kitを支えており、このシステムは現在100以上のコンポーネントに及び、実際の企業製品であるCube Cloudを動かしている。
Tastyには型付きコンポーネントAPI、サブ要素、SSR統合、ゼロランタイム抽出、エディタツール、リンティング、トークン、レシピなども含まれている。GitHubでソースコードを確認でき、プレイグラウンドでブラウザ上で試すことができる。完全な言語と機能セットについてはドキュメントを参照のこと。フィードバックはGitHub Issuesで受け付けている。
氏の主張はこうだ:コンポーネントの状態は記述しやすく、曖昧にしにくくすべきである。
詳細はWhy I spent years trying to make CSS states predictableを参照していただきたい。