UIコンポーネントライブラリにおけるスタイルの提供方法
複数のプロダクトやモノレポで共通利用するパッケージとして UI コンポーネントライブラリを作る時、導入先となるプロダクトやパッケージは多くの技術スタックやアーキテクチャを利用しています。ライブラリはそれらに合わせて設計やアーキテクチャを考えないといけません。その中で重要な点の 1 つはスタイルをどのように提供するかということです。不適切なスタイル提供方法はライブラリの利用を妨げる可能性があります。
この記事ではいくつかのスタイルの提供方法とそれらのメリット・デメリットや選定の基準をまとめてみました。
少しでも皆様の技術選定の助けになれば幸いです。
スタイルの提供方法の選択肢
内部で利用するライブラリに関わらず、UI コンポーネントライブラリやパッケージとして提供する上で使う側がどのようにスタイルを利用するかを選択する必要があります。プロジェクトの状態にもよって取れる選択肢は変わってきます。考えられる選択肢としては下記があります。
- 1 つの CSS ファイルを提供する
- 各コンポーネントの CSS ファイルをそれぞれ提供する
- スタイリングライブラリのプリセットとして提供する
- CSS in JS として提供する
1. 1つのCSSファイルを提供する
ライブラリ側で 1 つの CSS ファイルをエクスポートして、アプリケーション側ではその CSS ファイルを自身のグローバルな CSS ファイルでインポートします。スタイリングライブラリを利用する場合、ビルド時に静的な CSS ファイルを生成します。
@import url('@tongari/my-components/styles.css');
メリット
- 導入方法がシンプル: アプリケーション側では 1 つのファイルのみをインポートし、global.css 内に 1 行追加するだけで全てのコンポーネントは最初からスタイルが当たった状態になります。
- 技術スタックによらず利用できる: 静的な CSS ファイルを提供するため、アプリケーション側でビルドする必要はなく、React Server Component などのサーバーでレンダリングされるコンポーネントでも利用できます。
- ライブラリ側の技術選定が自由: Pure な CSS を利用したり、CSS Modules やスタイリングライブラリを利用する方法まで幅広い選択肢があります。
デメリット
- CSSファイルのサイズが大きくなる: 利用していないコンポーネントも含む全てのスタイルを 1 つの CSS ファイルにまとめて提供するため、コンポーネントの数が増えてスタイルが多くなるにつれてサイズが大きくなってしまいます。
2. 各コンポーネントの CSS ファイルをそれぞれ提供する
この方法では提供するコンポーネント毎に CSS ファイルを用意し、コンポーネントの JS ファイルや型定義ファイルと一緒にエクスポートします。アプリケーション側ではコンポーネントをインポートする際に CSS ファイルも一緒にインポートします。
import { Button as CommonButton } from '@tongari07/my-components/Button';
import '@tongari07/my-components/Button/styles.css';
export const Button = (props) => {
return <CommonButton {...props}/>
}
メリット
- バンドルサイズが小さくなる: アプリケーション側はコンポーネント毎のスタイルのみをインポートできます。
- パフォーマンスが最適化される: Next.js などのビルド時にページ毎に CSS ファイルをまとめるなどの最適化を行うフレームワークを利用している場合、最適化の対象になります。
- 技術スタックによらず利用できる: 静的な CSS ファイルを提供するため、アプリケーション側でビルドする必要はなく、React Server Component などのサーバーでレンダリングされるコンポーネントでも利用できます。
デメリット
- 個別にインポートが必要: アプリケーション側では新しいコンポーネントをインポートするたびに CSS も同じようにインポートする必要があります。UI コンポーネントライブラリは多くのコンポーネントを提供することになるため、コストが大きくなります。
- ライブラリ側の技術選定の選択肢が狭まる: 調査した限りそれぞれのコンポーネントの CSS ファイルを別々に生成できる CSS in JS ライブラリが見つからず、おそらくこの方法はピュアな CSS か CSS Modules を利用することが前提になります。Tailwind CSS や CSS in JS ライブラリを使って開発したい場合はこの方法を選択できません。
3. スタイリングライブラリのプリセットとして提供する
Tailwind CSS や CSS in JS ライブラリのプリセットをエクスポートし、アプリケーション側はそれを自身の設定にてインポートして組み込みます。UI コンポーネントライブラリでは同じ設定ファイルを利用してコンポーネントを実装し、アプリケーション側で自身のプロジェクトのコードと一緒にビルドすることで、ライブラリ側はビルド無しでコンポーネントを提供できます。
メリット
- ライブラリの最適化が行われる: アプリケーション側でビルドするためライブラリの最適化の恩恵を最大限に享受できます。例えば Tailwind CSS や Panda CSS では、ビルド時に重複しているスタイルを排除して CSS ファイルを生成してくれますが、この最適化にライブラリのコンポーネントも含めることができます。
デメリット
- 技術スタックを強いることになる: 上述の通りアプリケーション側でビルドする必要があるため、違うスタイリングライブラリを利用している場合、新たにインストールしてビルドプロセスに組み込む必要があります。
- 設定の競合が発生する可能性がある: 同じスタイリングライブラリを利用している場合は新たにインストールする必要はありませんが、ライブラリで提供している設定と既存の設定の間に競合が発生するリスクがあります。
4. CSS in JS として提供する
この方法では CSS ではなく、CSS in JS ライブラリを利用してコンポーネント内部でスタイルを定義したものをそのまま JavaScript ファイルとして提供します。RSC などのサーバーでレンダリングされるコンポーネントが出る以前のメジャーな方法です。主に Emotion や styled-components などを利用するのが一般的です。
この方法のメリットについては多くの記事がすでに存在するため、ここでは割愛します。
デメリット
- Server Componentで利用できない: この方法はランタイムで実行されるためコンポーネントを Client Component として提供する必要があります。App Router の強みである Server Component で利用できず最適化が行えなくなります。
スタイルの提供方法を決める上で重要な観点
ここからは共通利用される UI コンポーネントライブラリの、スタイルの提供方法を決めるために重要な観点を提示します。どれも重要ではありますが中でもプロダクトの状況や特性、チームのモチベーションなどを考慮して優先度を決めることが重要です。
導入のしやすさ
各プロダクトチームはそれぞれに優先すべきタスクがあり、UI コンポーネントライブラリへの以降は大きな負担になります。導入の負担をなるべく減らすことで、早い段階から移行してもらうことができます。どの方法も一長一短ありますので、プロダクトの状況やチームのモチベーションを考慮して選ぶことが重要です。
例えば 1 つの CSS ファイルを提供する方法は、一度スタイルをインポートするだけです。これはプロダクト側のスタイリングライブラリにほとんど影響されないため、導入が簡単です。
技術スタックを強いることを避ける
UI コンポーネントライブラリを導入するために新しいライブラリをインストールしないといけない、という状況は避ける必要があります。依存先が増えることになりますので、今後のメンテナンスの負担が増えてしまいます。
例として Tailwind CSS のプリセットを提供して、アプリケーション側で組み込んでビルドする場合、アプリケーション側が Tailwind CSS を使っていない場合、新しくインストールしてビルドのフローに組み込む必要があります。これは利用者に新しい技術スタックを強いると同時に導入もしづらいです。
例外として多くのプロダクトで同じライブラリを利用しているケースや、新しいライブラリを推進して統一していきたいというケースでは有効です。UI コンポーネントライブラリを起点にライブラリの統一が行われるので、より推進がしやすいことが考えられます。
パフォーマンス
スタイルの提供方法によっては、アプリケーションの初期読み込み時間やランタイムパフォーマンスに大きな影響を与えることがあります。例えば、全てのスタイルを 1 つの CSS ファイルにまとめて提供する方法は、初期読み込み時に大きなファイルをダウンロードする必要があるため、初期表示が遅くなる可能性があります。一方、各コンポーネントごとに CSS ファイルを提供する方法は、必要なスタイルのみを読み込むため、初期読み込み時間を短縮できます。
モダンなフレームワークとのシナジー
UI コンポーネントライブラリの導入先となるプロダクトの多くが App Router を使うプロダクトである場合、Emotion や styled-components などのランタイムでスタイルが生成される方法を利用するライブラリでコンポーネントを作ることが選択肢から外れます。
Next.js の App Router では、コンポーネントはデフォルトで Server Component となり、最適化が行われます。その際、ライブラリのコンポーネントで Emotion が利用されている場合、Client Component として提供する必要があります。するとプロダクト側では Server Component のメリットを享受できなくなってしまいます。UI コンポーネントライブラリが提供するコンポーネントは汎用的なものが主なので、プロダクトの至る所で利用されます。そのコンポーネントが原因でプロダクトのパフォーマンスを損ねてしまうのは避ける必要があります。
各方法の比較表
提供方法 | 導入のしやすさ | 技術スタックの強制 | パフォーマンス | RSC対応 |
---|---|---|---|---|
1つのCSSファイルを提供 | 高い | なし | 中程度 | 対応 |
各コンポーネントのCSSファイルを提供 | 中程度 | なし | 高い | 対応 |
スタイリングライブラリのプリセットとして提供 | 低い | 同じライブラリの導入が必要 | 非常に高い | 対応 |
CSS in JSとして提供 | 中程度 | なし | 中程度 | 非対応(RSC) |
まとめ
UI コンポーネントライブラリの技術選定において、具体的なスタイリングの技術選定に目が行きがちですが、まずは UI コンポーネントライブラリのユーザーであるプロダクトの開発者にどのようにスタイルを提供するかを検討する事に目を向ける必要があります。プロダクトの特性や技術スタックなどによってスタイルを提供する方法を検討する必要がありますので、丁寧に議論を進めることが重要です。
私たちのチームでは、これらの観点を総合的に考慮し、「1 つの CSS ファイルを提供する」方法を選択しました。この方法は導入のしやすさとメンテナンスの容易さを重視しつつ、RSC にも対応できるため、最もバランスが取れた選択肢と判断しました。
皆様のプロジェクトにおいても、上記の比較表と選定基準を参考にして、最適なスタイル提供方法を選んでいただければ幸いです。
参考にさせていただいた記事
Discussion