🎨

React で CSS を書く方法の選定;「Tailwind を使うと名前をつけなくてよい」とは何か

2022/11/09に公開約5,900字

先日 React で CSS を書くときに用いるライブラリを選定しました。その選定理由を備忘録として本稿を書き留めるとともに、各ライブラリを抽象化レイヤに着目して検討します。そして、Tailwind CSS のメリットとして語られる「名前を考える必要がなくて良い」とはどういうことかの説明を試みます。

React コンポーネントにスタイルを書く方法

React でアプリケーションを開発する際、スタイルを記述する方法として次の 5 つが挙げられます。

  • グローバルの CSS
  • インラインスタイル
  • コンポーネントスコープの CSS
  • CSS in JS
  • Utility-first CSS

それぞれについて説明します。

グローバルの CSS

CSS ファイルを記述し <link> で HTML と紐付ける、昔ながらの CSS です。記述したスタイルが HTML 全体に適用されるため Web アプリケーション開発においては管理が難しく、現在では避けられるようになりました。

React での Web アプリケーション開発でこれを選択するメリットはほとんどありません。以降では扱わないことにします。

インラインスタイル

React コンポーネントの JSX に直接スタイルを記述するパターンです。

function SomeComponent() {
	return (
		<p style={{ fontWeight: "bold" }}>
			something
		</p>
	);
}

この方法はパフォーマンス上の問題があるため、一般的には利用されません。以降では扱わないことにします。

コンポーネントスコープの CSS

昔ながらの CSS を記述しますが、トランスパイラによってスコープをコンポーネントに絞ることができる書き方です。

// SomeComponent.module.css
.hoge {
	font-weight: bold;
}

// SomeCompoennt.jsx
import styles from "./SomeComponent.module.css"

function SomeComponent() {
	return (
		<p className={styles.hoge}>
			something
		</p>
	);
}

React では CSS Modules が、Vue では Scoped CSS がよく利用されます。スコープが絞られている、旧来の CSS の書き方を踏襲できるなどのメリットから多く採用されています。

CSS in JS(styled 記法)

スタイルを内包したコンポーネントを実装する記法です。styled-components や emotion, vanilla-extract, linaira など多くのライブラリで提供されており、人気のある記法です。

const P = styled.p`
	fontWeight: bold;
`;

function SomeComponent() {
	return (
		<P>
			something
		</P>
	);
}

コンポーネントに限定されたスコープに加え、動的なスタイルが書きやすい、TypeScript の恩恵が受けられるなどのメリットがあります。

CSS in JS(CSS Prop 記法)

スタイルを JSX に直接記述する記法です。styled-components, emotion などのライブラリによって実現されます。


function SomeComponent() {
	return (
		<p css={{ fontWeight: "bold" }}>
			something
		</p>
	);
}

インラインスタイルとほとんど同様の書き方ができます。この記法は babel plugin によって styled 記法にトランスパイルされるため、インラインスタイルにあったパフォーマンスの問題が解消されています。

Utility-first CSS

あらかじめ定義された CSS class を組み合わせてスタイルを実現する記法です。Tailwind CSS によって広く知られるようになった記法です。xstyled, chakra-ui などもこのカテゴリに分類されると思います。

function SomeComponent() {
	return (
		<p className="bold">
			something
		</p>
	);
}

これらの記法の違いは何か

これらの手法を見比べて、それぞれの違いがなにかを考えます。「スタイルの書きやすさ」や「パフォーマンスの良し悪し」は当然それぞれ異なりますが、これらは(重要ではあるものの)表面的な差異であり、アプリケーション設計上の問題ではありません。

私は「どこにスタイルの抽象レイヤをおくか」が異なっていると考えます。

1. 抽象レイヤを持たない記法

特別な抽象レイヤを導入しない記法として、次の 2 つが該当します。

  • インラインスタイル
  • CSS in JS (CSS prop 記法)

つまり、これらの記法では HTML element に適用される CSS をそのまま、JSX に直接記述します。本来の CSS とは kebab-case と camelCase の違いなどの表記の違いこそありますが、書いているものは本質的には同一です。

2. CSS スタイルの宣言を抽象レイヤとする記法

CSS スタイルの宣言とは background-color: red; などのような、CSS プロパティと値の組(key-value のペア)を指します。CSS スタイルの宣言を抽象レイヤとする記法は次のものです。

  • Utility-first CSS

例えば Tailwind CSS では、background-color: rgb(220 38 38);bg-red-600 というユーティリティクラスとして提供されています。つまり背景色の宣言(background-color: rgb(220 38 38);)が bg-red-600 によって抽象化されているといえます。

3. コンポーネント内の DOM 要素に適用されるスタイルを抽象レイヤとする記法

「DOM 要素」とは divp のことです。「DOM 要素に適用されるスタイルを抽象レイヤとする」とは、たとえば <div className="ok-button"> のように特定の要素に対するスタイルに名前がつけられる(抽象化できる)記法のことです。この記法に該当するものは以下の 2 つです。

  • CSS in JS (styled 記法)
  • コンポーネントスコープの CSS

これらは書きやすさは異なりますが、抽象化しているのものは同じです。

必要な・必要ではない抽象レイヤは何か

抽象レイヤによって記法を 3 つに分類しました。ここで、我々が必要としている抽象レイヤはどれでしょうか。

コンポーネントによって Web アプリケーションを構築する我々は、統一的なデザインを保持するために CSS スタイルの値 もしくは CSS スタイルの宣言 を抽象化したい、と私は考えます。統一的なデザインの保持とは、テーマ(ダーク or ライトテーマ)やデザインシステムなどのことです。

つまり、抽象化によって次のような記述を実現する必要があります。

  • 配色を primary-color や secondary-color, background-color などの名前で扱う
  • margin, padding の大きさを small や xl などの名前で扱う

この抽象化に該当するレイヤを提供するのは「2. CSS スタイルの宣言を抽象レイヤとする記法」つまり Utility-first CSS です。

Utility-first CSS 以外を採用する場合、これに相応する抽象化を導入しなければなりません。たとえば、CSS Variables で primary-color を定義したり、React 側(JavaScript 側)でテーマ機構を用意して色情報を管理したりする必要があります。

逆に、「3. コンポーネント内の DOM 要素に適用されるスタイルを抽象レイヤとする記法」(styled 記法やコンポーネントスコープの CSS)にあたる抽象化は必要でしょうか?

私にはこの抽象レイヤは設計上は何も解決していないようにみえます。なぜならこの抽象レイヤの役割は React コンポーネントと完全にオーバーラップしているからです。

// styled 記法の例(再掲)
const P = styled.p`
	fontWeight: bold;
`;

もし上記の P が複数の箇所から利用されるのであれば P に相当する React コンポーネントを実装すればよく、P が単一の箇所からしか利用されないのであれば、改めて P という名前をつけるのではなく JSX の <p> 要素にスタイルを直接記述するほうが良いと考えます。疎結合にする理由もなしにスタイルと DOM の記述を分離する必要はありませんし、分離するに足る理由があるなら React コンポーネントとして括りだせばよいのです。

styled 記法がなぜこうなっているかというと、パフォーマンスの都合上 redner メソッドの外にスタイルの定義を置く必要があるためと思われます。このために styled-components や emotion は無用な抽象レイヤを導入してしまっているのです。

この無用な抽象レイヤによって、styled-components や emotion では名付けに困ってしまうのではないでしょうか。ブログ等で Tailwind CSS が紹介される際「名前をつける必要がない」というメリットが挙げられるのをよくみますが、これは抽象レイヤが原因となって発現する困難であると思います。

何を採用すべきか

ここまでの議論から、「CSS スタイルの値」や「CSS スタイルの宣言」だけを抽象化するのが適切であることを説明しました。これは Tailwind CSS によって実現できます。

Tailwind CSS は強力な抽象レイヤを提供しており、「bg-red-600」や「text-sm」などのデザイントークンを半強制することができます。

しかし、抽象レイヤが厚いがゆえに発生する問題点もあります。学習コストの高さです。
Tailwind CSS では抽象レイヤと同時にプロパティ名の変換が導入されています。たとえば background-color を指定したい場合は bg-***font-size を指定したい場合は text-** などの変換ルールを覚える(もしくは毎回調べる)必要があります。

Tailwind CSS を用いてコンポーネントを実装すると大量の className が横に並ぶことになるので、文字数を短くしたいのは理解できます。しかし個人的に毎回プロパティ名を調べながら実装するのは得られるメリットに釣り合わないと感じました。

そこで、今回は styled-components の CSS Props 記法を採用することにしました。CSS Props 記法は「1. 抽象レイヤを持たない記法」です。したがって、テーマやデザインシステムのためには別に抽象レイヤを導入する必要があります。

styled-components の標準的な方法としては styled-theming などがあるようですが、私は React 側で独自のテーマ機構を用意しました。{ primaryColor: "#xxxxxx" } のようなオブジェクトを React context でコンポーネントに伝播させるシンプルな方法です。

// こんな雰囲気になります
export default function SomeComponent() {
  const designTokens = useDesignTokens();

  return (
    <p
      css={{
        color: designTokens.primaryColor,
      }}
    >
	hogehoge
    </p>
  );

必要以上に複雑でなく、不要な抽象レイヤがないため簡潔な記述が可能です。また、CSS-in-JS のメリット(TypeScript による型検査など)を享受することができます。

まとめ

  • スタイルの何を抽象化するかという視点から React での CSS 技術を整理しました
  • styled 記法は必要のない抽象レイヤを導入しており、「名前をつけるのが難しい」のは抽象レイヤから表出した難しさであることを説明しました。
  • これは「書きやすい/書きづらい」という表面的な問題ではなく設計上の課題です。そして Tailwind CSS はそれを解決しています
  • Tailwind CSS の抽象化レイヤは厚すぎるので styled-components の CSS Props がちょうどよさそうにみえました

Discussion

ログインするとコメントできます