こんにちは、ラクスル株式会社でエンジニアをしている宮﨑圭司(@miyahkun)です。皆さんは特定の技術選定について「もっとこうすればよかった」「ここはうまく機能している」などと考えたことはありますか。
私が自社アプリケーション開発においてCSSライブラリvanilla-extractの採用を決めたのは2023年4月のことでした。この経緯については、これまでも弊社ブログ[1]やオンラインイベント[2]でも紹介してきましたが、2年ほどの時間を経てアプリケーションはさらに大きくなり、エコシステムは思いも寄らない速度で変化しています。
このような状況を踏まえ、本記事では「技術選定から一定の時間が経過した今、現状を見つめ直し、次の一歩をどう進めるか」というテーマについて考えていきます。
vanilla-extractの導入とその経緯
ダンボールワンのシステム刷新とCSS戦略
ラクスル株式会社では、ネット印刷の「ラクスル」を筆頭に多種多様なサービスを展開しており、その一つに「ダンボールワン」という梱包資材を扱うECサイトがあります。一口に梱包資材といっても、ダンボール箱から宅配袋、メール便ケース、プチプチ[3]などの緩衝材、テープ類まで幅広く取り扱っています。サイズや厚み、印刷まで指定可能なオーダーメイド商品にも対応しており、個人の引越しから企業の大量発送まで、さまざまなニーズに応える商品展開となっています。
▲ RAKSULグループ
ダンボールワンは2023年に合併によってラクスルの一員となったサービスで、サイトのUIデザインが他のラクスルのサービスと大きく異なるのが特徴です。また、当初のシステム構成は、PHPアプリケーション一つのモノリス構成でした。
そこで、より開発の生産性を高めるため、一部の機能を新しいアプリケーションへ切り出すことになりました。この一環としてWebフロントエンド部分を刷新するべく新しいアプリケーションを立ち上げ、これに伴いCSSライブラリの検討も行った結果、最終的にvanilla-extractを採用しました。
▲ システム構成の変化
CSSライブラリの検討
vanilla-extractとは
vanilla-extractは、TypeScriptでCSSを記述できるCSS-in-JSライブラリです。ビルド時にCSSファイルを生成するため、ランタイムでJavaScriptによって動的にスタイルを適用する場合に必要となるオーバーヘッドがありません。また、静的型チェックによって、クラス名の参照ミスや削除忘れを防止できるほか、TypeScriptの値として色などのデザイントークンを指定することで保守性の高いスタイリングが可能です。加えて、CSS Modulesのように局所的なスタイルスコープを提供してくれるので、グローバルなスタイル汚染を防ぐことができます。
【要件定義】CSSライブラリ選定におけるシステム要件
vanilla-extract採用の経緯を説明するには、システム要件とフレームワーク選定について先に触れておかなければなりません。
技術選定において、システム要件を漏れなく洗い出すことは肝心です。要求を達成できない技術スタックでは元も子もありません。本件は既存システムからの移管であるため、既存システムとの差分を意識する必要がありました。「現状維持したいこと」「改善したいこと」「切り捨てたいこと」といった観点です。これらの情報を集めるためには、エンジニア以外の職種の人とも密に議論することが重要です。その中で洗い出された要件の一部を紹介します。
- 数百カテゴリ、数万商品に関する既存のHTMLやCSSのコンテンツ資産を組み込めること。また、将来的に、必要に応じてモダンな実装への移行が可能であること
- プロダクトマネージャーや事業部長と、既存のコンテンツ管理方法が抱える問題を共有
- 検索サイトの表示順位が売り上げに直結しやすい商材であるため、SEO施策に対して柔軟に対応できること
- 広告を管理しているマーケティングチームと、旧ページの移行方式やフレームワークの制約がSEO評価に悪影響を与えるか議論
- 将来的に弊社デザインシステム「kamii」を導入する場合に、スムーズな移行が可能であること
- デザイナーやプロダクトマネージャー、事業部長と、合併した会社のサービスをどのように統合していくか議論。当時は結論に至らなかった
【前提技術】Next.jsとApp Routerの制約と選定理由
これらの要件を満たすために、フレームワークにはNext.jsを選択し、当時正式リリースされたばかりのApp Routerを採用しました。これはNext.js 13で導入されたルーティングシステムで、React Server Componentsを利用したレンダリングが可能です。
今回のアプリケーションは、Node.jsサーバーを立ててサーバーサイドレンダリングを行います。この場合、App RouterはサーバーサイドでReact Server Componentsを実行するため、ブラウザ環境に依存するランタイムCSS-in-JSライブラリ(styled-componentsやemotion等)は使用できません。この制約から、私たちはvanilla-extractやKuma UIのような、ビルド時にCSSを生成する方式のCSS-in-JSライブラリについて検討しました。Tailwind CSSやCSS Modulesも有力な選択肢でした。
【他候補の検討1】Tailwind CSSの利点と採用しなかった理由
このようなフレームワークの制約も加味してCSSライブラリの調査を開始しました。まずはじめに候補に挙がったのはTailwind CSSです。Tailwind CSSは用意されたスペーシングや色などのデザイントークンを元にユーティリティクラス(特定のスタイルプロパティを制御するCSSクラス)を提供します。昨今、新しくWebサイトを作るなら、多くの場合Tailwind CSSが第一候補に挙がるでしょう。
また、ラクスルには「kamii(カミー)」という独自のデザインシステムが存在しますが、kamiiではTailwind CSSのプラグインとしてデザイントークンとコンポーネントのスタイルを提供しています。社内での採用事例もありました。
しかし、有力候補であったものの、以下の理由で私たちがTailwind CSSを採用することはありませんでした。
- ダンボールワンの元々の実装ではデザイントークンが明確には整備されていない
- 加えてデザイントークンを整備する時間を割けない状況
- ラクスル加入後のデザインルールが決まっておらず、kamiiを導入するかどうか未確定
【他候補の検討2】CSS Modulesの利点と最終的に採用しなかった理由
次にCSS Modulesを検討しました。これはNext.jsのドキュメントで当時、最も推奨されており、フレームワークが標準サポートしている点で安心感がありました。PostCSSの工程で一部の社内プラグインが動作しないという問題が発生したものの、代替手段はありました。
検討の結果、CSS Modulesはひととおり要件を満たしていたのですが、より開発の生産性が高く、管理しやすいライブラリがあるのであれば、そちらも検討してみることになりました。
【最終選定】vanilla-extractを採用した理由とリスク対策
そこで、Next.jsのドキュメントで紹介されているライブラリの一つ、vanilla-extractを検討しました。
vanilla-extractの基本的な使い方は次のとおりです。この例では、画面幅に合わせて背景色が変化するボタンコンポーネントを書いています。
// button.css.ts
import { style } from '@vanilla-extract/css';
export const button = style({
background: 'blue',
'@media': {
'screen and (min-width: 768px)': {
background: 'red',
}
},
});
export const unusedTextStyle = style({
fontSize: '1rem',
});
// button.tsx
import * as styles from './button.css';
export const MyButton = ({ content }) => (
<button className={styles.button}>
{content}
</button>
);
vanilla-extractではxxx.css.ts
という名前のファイルにスタイルを書くのがルールです。style
関数を使用して、CSSプロパティをJavaScriptのオブジェクト形式で記述します。この関数がビルド時に実行されてクラス名を返します。そして、コンポーネントでは生成されたクラス名をインポートします。
なお、この例のunusedTextStyle
はどこのファイルからも参照されていないスタイルですが、エディターの機能やts-pruneのようなツールを使用することで、このような削除漏れのスタイルを簡単に見つけられます。これはvanilla-extractがTypeScriptを利用しているため得られる恩恵です。
実際にvanilla-extractをチームで試用したところ、メンバーからは以下のような良い反応がありました。
- TypeScriptの型情報でコード補完が効くため、実装スピードが上がりそう
- CSSプロパティを一つずつ記述する従来の実装スタイルと似ているので学習コストが低い
- 長期間メンテナンスする上で、使用中のスタイルかどうか自信を持って判断できるのはうれしい
一方で、社内外含めて利用実績が少なく、ライブラリの継続的なメンテナンスに懸念がありました。そこで私たちは、vanilla-extractから別の仕組みへ移行する場合に備え、二つのリスク軽減策を講じました。
一つ目は、有力な代替手段であるCSS Modulesへの変換を試し、その難易度を確認することでした。vanilla-extractは「ビルド時にCSSファイルを生成する」仕組みであるため、内部実装に手を加えればこれは比較的容易に変換できそうでした。
二つ目は、「vanilla-extractの複雑な機能に依存しない」という方針を決めたことです。それというのも、vanilla-extractにはSprinklesやRecipesという、Tailwind CSSのユーティリティクラスのようにスタイルをまとめて定義する仕組みがありますが、このような機能を利用すると、CSS Modulesへの変換が難しくなるからです。
結果として、私たちのプロジェクトでは、vanilla-extractを採用し、基本的な機能に絞って利用することになりました。
環境の変化と再考のきっかけ
ここまでvanilla-extractの導入の経緯を説明してきましたが、アプリケーションを立ち上げてから2年ほどが経過し、社内外の環境は大きく様変わりし、今後のCSS戦略に影響を与える変化がいくつかありました。
ダンボールワンへのTailwind CSS導入開始
開発が進む中で、元からあるダンボールワンのデザインと混在する形でデザインシステム「kamii」を少しずつ導入することになりました。kamiiはTailwind CSSで実装されていますが、一般的に既存アプリケーションに対して後からTailwind CSSを導入するのは難しいとされています。なぜなら、既存のスタイルとTailwind CSSのユーティリティクラスが競合しやすいためです。私たちも同様の問題に直面しました。
Tailwind CSSでは「Preflight」というベースとなるスタイルシートが使用されます。これはブラウザ間でのデフォルトスタイルの違いを統一するためのもので、reset.cssと同様の機能を果たします。私たちのアプリケーションでは、最初にreset.cssが導入されていたため、これらが競合してしまいました。
この問題を解決するために、特定のクラスが付いた要素配下にだけTailwind CSSのスタイルが優先的に適用されるようTailwind CSSの設定を変更しました(注意:次のコードはTailwind CSS v3でのみ動作確認しています)。
tailwind.config.tsはこのようになっています(概要)。
const config = {
// 他の設定...
important: 'kamii-element',
corePlugins: {
preflight: false,
},
plugins: [
require('@raksul/kamii-css')({ // kamiiが提供するTailwind CSSのプラグイン
respectImportant: true,
}),
plugin(function ({ addBase }) {
// Preflightのクラスに.kamii-element を付与する処理
// ...
addBase(...) // 書き換えたスタイルシートをベースのスタイルとして差し込む
}),
],
} satisfies Config;
この設定により、次のようなPreflightのスタイルシートが生成されます:
.kamii-element h1 {
font-size: inherit;
font-weight: inherit;
}
使用時は以下のように.kamii-element
クラスでスコープを区切ります:
<div className="kamii-element">
<label class="Checkbox">
<input type="checkbox" class="Checkbox-input" />
</label>
</div>
kamiiは、kamii-cssというTailwind CSSプラグインで、@apply
を利用して定義されたコンポーネントクラスを提供しています。例えばチェックボックスコンポーネントなら.Checkbox
などのクラスを指定します。
先ほどのtailwind.config.tsの設定により、kamiiのスタイルは.kamii-element :is(.Checkbox)
というセレクターで生成されます。これにより、.kamii-element
配下の.Checkbox
だけにkamiiのスタイルが適用される仕組みです。
もっと賢い方法もあるかもしれませんが、私たちのアプリケーションではこの仕組みで、既存のvanilla-extractによるスタイリングを維持しながら、部分的にkamiiのコンポーネントを導入することができました。
余談ですが、このような異なるスタイルの混在は今後もっと楽に実現可能になるはずです。なぜなら、CSSの標準仕様として @layer
(MDN: @layer)や@scope
(MDN: @scope)のようなスタイルを隔離する機能が取り込まれたからです。これらのブラウザサポートが拡大して気兼ねなく利用できるようになれば、フロントエンドエンジニアはより幸せになれるでしょう!
「kamii」の浸透とCSS戦略への影響
現在、kamiiの導入から1年以上が経過し、kamiiのデザイントークンがページの広範囲に浸透しつつあります。部分的に利用されているものも含めると、3分の1以上のコンポーネントでデザイントークンが参照されています。
最初はボタンなどの基礎的なコンポーネントから始まり、現在は複合的なコンポーネントの実装にも利用されるようになりました。今後の開発でも、多かれ少なかれkamiiに則った実装が行われる見込みです。
このようにkamiiが浸透する中で、vanilla-extractとTailwind CSSの使い分けについて改めて考える機会が増えました。そんな折、Tailwind CSS自体にも大きな変化が訪れます。
Tailwind CSS v4の「CSS-First Configuration」がもたらす可能性
2025年1月にTailwind CSS v4がリリースされました。大きな変更がいくつも含まれているのですが、その中でも「CSS-First Configuration」と呼ばれる方針が私たちの技術選択にも大きな影響を与えようとしています。
これは従来のJavaScriptベースの設定記法からCSSベースの設定記法へと移行する新しいアプローチであり、この結果、カスタムプロパティやデザイントークンをCSSの標準的な記法で定義できるようになりました。この変更は「Tailwind CSS以外のライブラリやインラインスタイルでの値の再利用を容易にするため」と説明されています。
この方針を私たちのチームはどう捉えるべきでしょうか。
例えば、次のコードでは、Tailwind CSS側でカスタムプロパティとして定義されている文字色--color-black-100
をvanilla-extract側で参照しています。この方法は元々私たちのアプリケーションでも活用していました。
export const pageText = style({
color: 'var(--color-black-100)',
});
しかし、この方法を多用するのはあまり好ましくありません。CSSカスタムプロパティは文字列として扱われるため、TypeScriptの型チェックの恩恵を受けられません。例えば、存在しない色名を指定してもコンパイル時にエラーが検出されず、vanilla-extractが提供する型安全性の利点が失われてしまいます。TypeScript/JavaScriptの世界で完結することがCSS-in-JSを最大限に活かす方法ですが、これはTailwind CSS v4のCSSベースの設定記法とは相容れないように思います。
生成AIに適したスタイリング方法
また、フロントエンド開発を取り巻く外部環境も大きく変化しています。Tailwind CSS v4 のような変化に加え、特に生成AIの普及は今後の技術選定にも影響を与えると考えられます。
生成AIによるコード生成が当たり前になりつつある中、WebフロントエンドにはどのCSSライブラリを使用するのが良いでしょうか。または、ライブラリを使わずに生のCSSを直接記述する方が良いでしょうか。明確な答えは出せませんが、CSSライブラリに関して、私はTailwind CSSの人気が今後、より高まると推測しています。
執筆時点での生成AIは、「UIを仕様通りに実装する能力」が低いと感じます。指示していない装飾をつけたり、誤った色を定義したりすることがあり、人間による手直しが発生します。このような状況を鑑みると、人間が介入することを前提としたメンテナンスしやすいコード出力は依然として重要です。例えば、一貫したCSSプロパティの使用や、テキストカラーなどを再利用可能にするCSSカスタムプロパティの定義など、いわゆるコーディングルールと呼ばれる「規約」です。人間と生成AIのどちらにも当てはまることですが、一定の規約に基づいてコードを書かなければ、メンテナンス性は日を追うごとに急速に低下していきます。
ライブラリを使わずに生のCSSで実装しているプロジェクトがあるとします。生成AIに規約を守らせるためにCursor RulesやCLAUDE.mdで指示しても、簡単に逸脱したコードが出力されるでしょう。しかし、生成AIによく学習されたOSSライブラリを指定しておくと、ライブラリが定めるルールに則ったコードになりやすいことは、実体験で感じています。この点に関して、Tailwind CSSのようなライブラリを「人間と生成AIの共通言語」と捉えてみると、私の中で腑に落ちるものがありました。
では、どのライブラリが適しているのでしょうか。非常に単純に考えると、生成AIに最も学習されているライブラリを選択するのが望ましいはずです。CSSの利用状況に関するサーベイである「State of CSS 2024」を見ると、最も利用されているのはTailwind CSSだと分かります。また、直接的な利用以外でも、Headless UIやshadcn/uiのようなライブラリで間接的にTailwind CSSを利用している場合もあり、世の中に公開されているコードが大量に存在します。このような観点で考えると、生成AI時代のCSSライブラリとして、Tailwind CSSは有力な選択肢と言えるでしょう。
戦略の見直しと今後の選択肢
Tailwind CSS中心の戦略への転換
現在、デザインシステム「kamii」の浸透という内的要因、生成AIの進化と相性の良いTailwind CSSという外的要因から、メインのCSSライブラリをTailwind CSSに入れ替える案がチーム内で検討されています。
しかしながら、アプリケーションの中に既存CSS資産を組み込む場合など、Tailwind CSS以外の方法で書きたいという要求も残っています。これに対して私たちは引き続きvanilla-extractを使うべきでしょうか。当初に立ち戻ると、私たちは技術選定時に次のことを期待していました。
- TypeScriptによる型情報の恩恵
- CSS-in-JSならではの値の取り回しやすさ
- 既存CSS資産を組み込むためにスタイルを柔軟に適用できること
大半の実装がTailwind CSSになればvanilla-extractのコード量は激減するため、1と2のメリットを享受できる場面が減ります。残された3を実現する方法はもはやvanilla-extractでもなくても良いでしょう。生のCSSで十分かもしれませんし、スコープ機能が欲しければCSS Modulesがあります。
vanilla-extractからCSS Modulesへの移行計画
この状況を加味して、私たちはvanilla-extractの記述をCSS Modulesもしくは純粋なCSSへ移行することを考えています[4]。これはvanilla-extract採用時点から想定していたシナリオであり、前述のように、その方法もある程度確認していました。
移行には、次の3ステップが必要です。
- vanilla-extractのスタイルファイル(例: header.css.ts)をCSS Modulesへ変換する
- 基本的にコンポーネント単位でスタイルを書いており、この単位でvanilla-extractのスコープ機能が動作している。CSS Modulesに変換する際はファイルが1:1対応するように変換する
- Tailwind CSSのPreflightを有効にする
- 部分的にTailwind CSSを適用する設定を削除して、現在使用しているreset.cssをPreflightで置き換える。この作業によりページ全体で差分が生じるが、私たちのチームではChromaticというVisual Regression Testingツールを導入しており、これにより変更前後の細かい差分をチェックしている
- Tailwind CSSのユーティリティクラスへ置き換える
- CSS Modulesを順次Tailwind CSSのユーティリティクラスに置き換える。この作業は急いで進める必要はないと思っており、何か編集するタイミングで少しずつ置き換えていく予定
現在、vanilla-extractをCSS Modulesに変換するCLIツールを実装中です。かなり単純な変換であるため、実際のアプリケーションでは追加調整が必要ですが、このようなツールを使用することで、このステップの作業は大幅に短縮できると思います。
技術選定では“少し先の未来”を見据える
私は、技術選定では、現在の要件だけでなく、少しだけ先の未来に必要になりそうな要件も考慮して技術やシステム構成を決めることが重要だと考えています。現時点で確定している要件への対応は当然ですが、それだけでは不十分です。
ここで私たちが当初想定していた「少しだけ先の未来の要件」を振り返ってみます。
- 要件:もしkamiiの必要性が高まれば導入できる状態にする
- 結果:設定は複雑だが、kamiiのスタイルを活用できている
- 要件:カテゴリや商品説明の膨大なコンテンツ資産をモダンな実装へ移行する
- 結果:数百カテゴリのコンテンツ資産をvanilla-extractとkamiiを活用して移行完了した
結果的には、要件を達成することができています。
プログラミングの世界には「YAGNI(You aren't gonna need it)」という言葉があります。「現時点で具体的な必要性がない機能は追加するな」という意味です。技術選定においても同様の考え方が適用できますが、ライブラリやシステムコンポーネントによっては簡単に変更できるものではない点に注意が必要です。したがって、将来必要になる可能性の高い要件については、最初から考慮しておくべきです。
ただ私は、技術選定の記事でこう言うのも何ですが、実際のところフロントエンド開発では、どのCSSライブラリを選ぶかよりも、サービスの将来像を描けるかどうかの方がより重要だと思っています(もちろんCSSライブラリを軽視しているわけではなく、開発者の皆さんはリスペクトしています)。
「システム要件の洗い出し」で触れたように、サービスを支える様々な職種の方とコミュニケーションを取ることで、現状サービスの気づきを得て、システム要件に関する全体像を把握する“土地鑑”をつけることができます。
さらに、昨今の技術革新を受けて、「どう実装するか(How)」は大部分を生成AIに任せられる日は遠くないと感じています。今後はより「何を作るか(What)」に重点を置く必要があるはずです。
おわりに:変化し続ける技術選定
本記事ではダンボールワンにおけるCSSライブラリの技術選定の経緯とこれからの展望について紹介しました。これまでの内容をまとめると次のようになるでしょう。
- 良い技術選定は詳細なシステム要件の絞り出しから生まれる。そのためには、サービス関係者と密に連携し、現在と将来の要求にアンテナを張ることが必要
- その上で、次に求められることに対して柔軟に対応できるライブラリを選ぶべき
- 確度の高いリスクへの対策としては、別のライブラリへの移行の可能性を考慮すべき
技術選定は決して一度きりのものではありません。サービスの変化、エコシステムの進化に合わせて継続的に見直す必要があります。本記事が皆さんの技術選定において何かの参考になれば幸いです。
編集:勝野久美子(トップスタジオ)
制作:はてな編集部
-
RAKSUL TechBlog「ダンボールワンのフロントエンドでゼロランタイムCSS in JSを採用してみた」 ↩
-
Speaker Deck「甘い香りに誘われてVanilla Extractを1年間運用してみた」 ↩
-
プチプチは川上産業株式会社の登録商標です。 ↩
-
なお、執筆時点では移行対象のReactコンポーネントは250個、それに対応したvanilla-extractのスタイルファイル(*.css.ts)もほぼ同数となっています。 ↩