Utility-first CSS の配置用ユーティリティだけ使う例

遅ればせながらこの前の年末年始に Tailwind CSSChakra UI を触った。(月並みな感想だが) utility-first CSS は書きやすい一方、前者は class に書かれる多くのユーティリティによりノイズが多く見え、後者は別の機能が載りすぎて too much という印象を受けた。これらを眺めていると、全てのスタイルを class や Props に直接は書かなくても utility-first CSS の利点を生かせるのではないかと思い始めた。

この記事では、Tailwind CSS との互換機能を持つ Windi CSS[1] を利用しつつ、utility-first CSS におけるノイズの少ない表記法として、配置用ユーティリティだけ使う場合の例を示す。

TL;DR

  • CSS プロパティには隣接する HTML 要素や画面全体の枠組みの一部として指定する物がある。それらは HTML に直接書いた方が意図やつながりを把握しやすい。
  • 装飾用 CSS プロパティは HTML 要素とその子に影響が閉じることが多い。HTML 全体を見る際にその詳細を知る必要はない[2]ため、HTML に直接書かれているとノイズになる。
  • marginflex 等、公開コンポーネントのルート要素に含めず使う側で指定すべきプロパティについては、律儀にクラス名を定義し別の場所に書くのは逆にノイズになるため HTML に直接書いた方が楽。
  • Windi CSS の Attributify 機能を使うといい塩梅で配置用ユーティリティの利用と見やすさを両立できる。
1
2
3
4
<div class="flex space-y-1 App__base">
<h1 class="flex-none App__header">No Title</h1>
<section class="flex-auto App__section" />
</div>
1
2
3
4
<div flex="~" space="y-1" className={baseStyle}>
<h1 flex="none" className={headerStyle}>No Title</h1>
<section flex="auto" className={sectionStyle} />
</div>

CSS プロパティと HTML 構造

※以下の分類は、この記事の説明上定義するものである。CSS の仕様として定められたものではないし、何かの技術用語でもない。

CSS のプロパティは、その用途や関係によって、配置用と装飾用とに分けられる。

「配置用プロパティ」

配置用 CSS プロパティは、隣接する HTML 要素や親要素、画面全体の中の一部として指定されるものだ。これらのプロパティの特徴として、その関心がその要素や子に関心が閉じない。例えば、marginflexgrid がこれに該当する。

1
2
3
4
<div class="App__base">
<h1 class="App__header" />
<section class="App__section" />
</div>
1
2
3
4
5
6
7
8
9
10
11
12
.App__base {
display: flex;
}
.App__base > * + * {
margin: 0.25rem;
}
.App__header {
flex: none;
}
.App__section {
flex: 1 1 auto;
}

「装飾用プロパティ」

装飾用 CSS プロパティは、その HTML 要素と子要素[3]のみで影響が閉じるものだ。例えば backgroundfont-family がこれに該当する。

1
2
3
4
<div class="App__base">
<h1 class="App__header" />
<section class="App__section" />
</div>
1
2
3
4
5
6
7
8
9
10
11
.App__base {
background: white;
}
.App__header {
background: darkgray;
font-weight: 600;
}
.App__section {
background: black;
font-weight: 400;
}

分けて書く

仮に配置と装飾それぞれのプロパティを完全に分けて書くとお互いのノイズは減るが、CSS で書くと普通は HTML での構造が平坦になってしまうためそれぞれの対象間の関係性は見えにくい。たとえば、flex を使うときは親と子のつながりが強いが CSS ではその親子関係は見えにくい。

こうしたものは HTML 構造を意識しつつ記述できると良いが、ここで utility-first CSS が生きる。

1
2
3
4
<div class="flex space-y-1 App__base">
<h1 class="flex-none App__header">No Title</h1>
<section class="flex-auto App__section" />
</div>

従来の CSS に書くプロパティであっても、ユーティリティを @apply で使ったり theme 関数を使ったりできる。デザイントークンを反映するユーティリティの利用を強制できないのは NG とする思想はある程度維持できる。

JSX を使うプロジェクトなら下の方法もある。

1
2
3
4
<div className={`flex space-y-1 ${baseStyle}`}>
<h1 className={`flex-none ${headerStyle}`}>No Title</h1>
<section className={`flex-auto ${sectionStyle}`} />
</div>
0

1
2
3
const baseStyle = "text-white";
const headerStyle = "text-gray-800 font-semibold";
const sectionStyle = "text-gray-900 font-normal";

外から当てるべき CSS

「なんとか-wrapper」

クラス名を考え付与すること自体は、セマンティックな認識単位を持つという点で有用だとは思う。しかし、marginflex 等を(公開)コンポーネントのルート要素に含めず[4]レイアウトの微調整を行うときも CSS のための名前が必要となる。こうした名前は大抵は「なんとか-wrapper」になってる。それはそれでセマンティックではあるが、中身を見ればその要素の役割は自明なので、ノイズになるし面倒くさい。

1
2
3
4
5
6
<div className="App__base">
<h1 className="App__title">No Title</h1>
<div className="App__section-wrapper">
<Section />
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
.App__base {
display: flex;
}
.App__base > * + * {
margin: 0.25rem;
}
.App__header {
flex: none;
}
.App__section-wrapper {
flex: 1 1 auto;
}

ユーティリティだけで良さそう

このような、レイアウト調整のためだけで特に意味のないクラス名の代わりにユーティリティを使うと記述が少なく書きやすい。そして、これは配置用 CSS プロパティであることから、周辺の要素もまとめて見れる方が好ましいため HTML にすべて書きたい。

1
2
3
4
5
6
<div class="flex space-y-1 App__base">
<h1 class="flex-none App__header">No Title</h1>
<div className="flex-auto">
<Section />
</div>
</div>

さらに先へ

Windi CSS の Attributify Mode

Tailwind CSS と互換機能を持つ utility-first CSS である Windi CSS には Attributify Mode という面白い機能がある。ユーティリティを class 一つに集中させるのではなく、関連するグループ毎に直接 HTML 属性に記述できる。記載するユーティリティが多いことによるノイズを減らし、意味上のまとまりも与えてくれるため読みやすい。

1
2
3
4
5
6
7
8
9
<button 
bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"
text="sm white"
font="mono light"
p="y-2 x-4"
border="2 rounded blue-200"
>
Button
</button>

本記事のように配置用ユーティリティだけの利用をしなかったとしても、可読性の面で積極的に使うべき機能だと思う。

Attributify の活用

Attributify を使えば、今までの例は以下のように書ける。配置用 CSS プロパティはユーティリティを使って HTML の属性として書き、それ以外は従来の CSS として書くため HTML 上で className に押し込む。かなり見やすいと思う。属性は ESLint カスタムルールや TypeScript の型で制限できる。

1
2
3
4
<div flex="~" space="y-1" className="App__base">
<h1 flex="none" className="App__title">No Title</h1>
<section flex="auto" className="App__section" />
</div>

検討すべき点

  • どの CSS プロパティを HTML に直接書くべきかはまだ明確でない
  • Utility の文字列と CSS Module や CSS in JS 用の文字列が class に並存するので混同しやすいかもしれない。(Attributify Mode を使わない場合)
  • Utility-first CSS の元々の思想とずれる
  • チーム開発の場合にルールをしっかり決める必要がある

まとめ

CSS プロパティを非公式に配置用と装飾用に分類し、前者を utility-first CSS のユーティリティを使い HTML に、後者を従来の CSS に分離して書く方法を例示した。


  1. Tailwind CSS はじめ他の utility-first CSS ライブラリでもある程度の活用は可能だろうと思う。 ↩︎

  2. 従来の考え方において HTML と CSS を分けて書くのに近い。 ↩︎

  3. 継承という形で ↩︎

  4. コンポーネントの関心という点で含めるのはアンチパターンとされる。 ↩︎