🧁

最高のCSS-in-JS体験!vanilla-extractで実現するZero-Runtimeと型安全なCSS

2024/07/31に公開

はじめに

vanilla-extract を利用することによって、Zero-Runtimeかつ型安全なCSS-in-JS体験が可能です。
今回はApp Router時代のスタイリング戦略で最高のCSS-in-JSライブラリ、vanilla-extract にDeepDiveしていきます。

vanilla-extract の生い立ち

vanilla-extract についてしっかりと理解していくために、まずは、どうして vanilla-extract ができたのか、 vanilla-extract の開発者であるMark Dalgleis氏はどのような人物なのかについて、みていきたいと思います。
https://kentcdodds.com/chats/04/22/mark-dalgleish-chats-about-vanilla-extract

vanilla-extract 誕生の背景

Dalgleish氏は、CSS Modules の共同作成者であり、所属するSeek社ではCSS Modulesとコンポーネントベースのアプローチが広く用いられていました。
https://github.com/css-modules/css-modules

しかし、 CSS Modules でアプローチしていく中で、プリプロセッサコードの複雑化やTypeScriptとの統合の不完全さ、さらにCSSカスタムプロパティのスコーピング問題に直面していました。

これら CSS Modules の課題を解決するため、JavaScriptを活用してBuild時にCSSを生成する方法を模索した結果、TypeScriptの型安全性とCSS変数のローカルスコーピングを組み合わせた手法を開発しました。
それが、 vanilla-extract です。

こうして vanilla-extract が誕生し、CSS-in-JSの開発者体験と静的CSSのパフォーマンスを両立させることに成功しました。

コミュニティとの連携

Dalgleish氏は、 vanilla-extract の開発初期から GitHub や X 、イベント等の開発者同士とのコミュニティとの連携を重視しており、ユーザーからのフィードバックを積極的に取り入れています。
https://github.com/vanilla-extract-css/vanilla-extract/discussions
https://discord.com/invite/6nCfPwwz6w

彼は、 vanilla-extract が(当時)まだ新しいプロジェクトであることを認識しており、ユーザーが試用し、その経験を共有することを奨励しています。
みなさんも一度公式のGitHubページを訪れることをおすすめします!

Yeah, I would love to hear what people think about this. If this is something that you want to try out in your projects, I would love to hear how it goes for you or any thoughts you have. Because we're pretty early. We're pretty open to the community here, and we're doing things quite differently. So, I'm always looking out to hear what people think about it. So, please reach out to me, let me know how you go with it.

vanilla-extract の満足度は、2023年度も2位!

State of CSS 2023で行われた調査では、 vanilla-extract の満足度が2位になりました!
多くの開発者がvanilla-extractで満足しているようです。
※State of CSS 2022年も満足度2位を獲得しています。

こうして、Dalgleish氏はコミュニティからの要望を日々重視することにより、ユーザーからの満足度も高く、結果としてNext.js 13 のApp Routerで正式にサポートされるようになりました。

※引用: State of CSS 「CSS-in-JS」 https://2023.stateofcss.com/ja-JP/css-in-js/(参照2024-07-31)
https://2023.stateofcss.com/ja-JP/css-in-js/

みなさん、 vanilla-extract の生い立ちについて理解できましたか?
次から本題に入っていきます。

App RouterによるCSS-in-JSの課題

App Router登場により、CSS-in-JSは、React Server Components (RSC)での使用と、パフォーマンスに影響を与えるオーバーヘッドの観点から大きな課題を抱えています。

Runtime CSS-in-JSやZero-Runtime CSS-in-JS、App Router時代でのスタリング戦略などを深く理解する必要があります。
これらについては、私が過去に記事を書いておりますので、参考にしてください。

  • Runtime CSS-in-JSやZero-Runtime CSS-in-JSの課題について詳しく知りたい方は、こちらをご覧ください。

https://zenn.dev/blueish/articles/e8bc1a5caf139f

  • App Router時代のスタイリング戦略については、こちらの記事をご覧ください。

https://zenn.dev/blueish/articles/78e3240881ad7e

vanilla-extractのざっくりとした説明

これらApp Routerの登場によって出てきた課題の解決策として、弊社では vanilla-extractを選択しています。
https://vanilla-extract.style/

vanilla-extract は、TypeScriptを使用したCSS開発を行うためのZero-Runtime CSS-in-JSライブラリです。
TypeScriptを使用するので、型安全な開発が行えることと、Zero-Runtimeで、Build時に静的CSSファイルを生成するので、実行時のオーバーヘッドがないことが特徴です。

CSS-in-JSの課題をvanilla-extractで解決

CSS-in-JSについては過去記事で説明していますが、改めてざっくりと説明させていただきます。

既存のCSS-in-JSライブラリの問題点

従来のCSS-in-JSライブラリには、いくつかの重要な課題があります。

まず、多くのライブラリがRuntimeでCSSを生成するため、特に大規模なアプリケーションにおいて、ページロード時間の遅延等のパフォーマンスへの影響が懸念されます。
また、型安全性が不十分なライブラリもあり、開発効率の低下につながる可能性があります。

Runtimeのオーバーヘッドとパフォーマンスへの影響

CSS-in-JSを導入する大きな利点の一つは、JavaScriptのロジックを駆使して動的にスタイルを管理できる点にあります。

しかし、スタイルが動的に生成されることによるRuntimeのオーバーヘッドが避けられません。
特に、大規模なアプリケーションや複雑なUIを構築する場合、コンポーネントごとにJavaScriptが実行され、CSSが生成・適用される過程がパフォーマンスのボトルネックとなることがあります。

このRuntimeオーバーヘッドは、ページロード時間の遅延や、ユーザー操作に対する反応速度の低下を引き起こす要因となり、全体的なパフォーマンスへ大きく影響します。

型安全性の欠如とスタイルの誤りによる開発効率の低下

CSS-in-JSは通常、スタイルをJavaScriptまたはTypeScriptで記述しますが、一部のライブラリでは型安全性が十分に担保されていないことがあります。
そのため、スタイルのプロパティや値に対する型チェックがなく、誤ったプロパティ名や値を指定してもエラーが発生しません。

結果として、スタイルの誤りがコンパイル時ではなく実行時に発見されることになり、バグの発見と修正に余計な時間がかかってしまいます。

この課題を vanilla-extract で解決!

こうした問題を解決するために登場したのが、Zero-Runtimeと型安全性を両立させた vanilla-extract です!

vanilla-extract は、Build時にCSSを生成し、実行時のオーバーヘッドを最小限に抑えることで、高速で効率的な開発を可能にします。
さらに、TypeScriptとの強力な統合によって、コンパイル時にスタイルのエラーを検出し、開発効率とコードの信頼性を大幅に向上させることができます。

vanilla-extractの特徴と利点

vanilla-extractの特徴と利点について、もう少し詳しくみていきたいと思います。


※GitHub 「vanilla-extract」
https://github.com/vanilla-extract-css/vanilla-extract?tab=readme-ov-file#-vanilla-extract (参照2024-07-29)

1. Build時のスタイル生成

🔥 All styles generated at build time — just like Sass, Less, etc.

SassやLessと同様に、すべてのスタイルをBuild時に生成します。
これにより、実行時にスタイルを生成するオーバーヘッドを排除し、ページのロード時間やパフォーマンスに悪影響を与えることなく、動的なスタイルを適用することができます。

2. 最小限度の抽象化

✨ Minimal abstraction over standard CSS.

vanilla-extract を使用して記述するスタイルは、基本的なCSSの構文とほぼ同じになります。

3. フロントエンドフレームワークとの互換性

🦄 Works with any front-end framework — or even without one.

さまざまなフロントエンドフレームワークとの互換性があります。
また、フレームワークを使用しなくても使用することができます。

4. ローカルスコープのクラス名

🌳 Locally scoped class names — just like CSS Modules.

CSS Modulesと同様に、ローカルスコープのクラス名を自動生成します。
異なるコンポーネント間でクラス名が衝突するのを防ぎ、スタイルが意図せず他のコンポーネントに影響を与えないようにします。

5. ローカルスコープのCSS変数

🚀 Locally scoped CSS Variables, @keyframes and @font-face rules.

CSS 変数、@keyframes、@font-face ルールを、ローカルスコープで定義することを可能にします。

6. 高レベルのテーマシステム

🎨 High-level theme system with support for simultaneous themes. No globals!

7. 変数ベースのcalc式生成

🛠 Utils for generating variable-based calc expressions.

CSS変数を使用したcalc式を生成するためのユーティリティを提供します。
これにより、動的で柔軟なレイアウトやスタイリングが可能になります。

8. 型安全なスタイル

💪 Type-safe styles via CSSType.

CSSTypeを使用して型安全なスタイルを実現します。
スタイルの定義時に型の誤りを事前に検出でき、IDEでの自動補完やエラー検出が強化されます。
型安全性により、コードの信頼性が向上し、デバッグ時間が短縮されます。

9. オプション: Runtimeバージョン

🏃‍♂️ Optional runtime version for development and testing

開発およびテストの段階で、Runtimeでのスタイル生成を可能にするオプションを提供しています。

10. オプション: 動的なRuntimeテーマ設定

🙈 Optional API for dynamic runtime theming.

Runtime時にテーマを変更するためのAPIも、オプショナルで提供しています。
ユーザーの好みや設定に基づいて、テーマを動的に切り替えることができます。
例えば、ダークモードやライトモードの切り替え、ユーザーごとのカスタムテーマの適用など。

以上が公式で記載されている特徴と利点です。
以下が独自の見解によるまとめです。

特徴まとめ① 【Zero-Runtime】 Build時に静的CSSを生成し、パフォーマンスへの影響を排除

vanilla-extract の特徴の一つは、やはり、Zero-Runtim CSS-in-JSであることです。
Build時に静的なCSSを生成し、Runtimeでのスタイルの計算を回避することで、パフォーマンスへの影響を最小にし、高速なアプリケーションを実現できます。

具体的には、実行時にCSSを生成する従来のライブラリでは、JavaScriptコードが実行され、スタイルが計算され、DOMに挿入されるという処理が必要でした。
これは、特に複雑なスタイル定義や多数のコンポーネントがある場合、パフォーマンスに悪影響を及ぼす可能性がありました。

一方、vanilla-extract はBuild時にCSSを生成するため、実行時には単に生成されたCSSファイルをロードするだけです。
これにより、実行時のオーバーヘッドが大幅に削減され、アプリケーションのパフォーマンスが向上します。

特徴まとめ② 【型安全性】 TypeScriptとの完全な統合により、スタイルの誤りを事前に検出

vanilla-extract は、TypeScriptとの完全に統合されているので、以下のようなスタイリングにおける型の恩恵を最大限に活用することができます。

【コンパイル時のエラー検出】
プロパティ名のタイプミスや無効な値の使用を、コンパイル時に検出できます。

【コード補完】
IDEの自動補完機能により、有効なCSSプロパティと値の提案をしてくれます。

【リファクタリングの安全性】
型チェックにより、大規模なスタイル変更も安全に行えます。

特徴まとめ③ 【CSSのモジュール化】 スタイルのカプセル化と再利用性の向上

vanilla-extract は、スタイルを個別のファイルに分割して管理するCSSのモジュール化をサポートしています。
各ファイルは、独立したモジュールとして機能するので、他のファイルとの衝突を回避できます。

CSSをモジュール化することにより、スタイルのカプセル化と再利用性の向上を実現します。
スタイルがカプセル化されることで、スタイルの衝突を防ぎ、コードの整理が容易になります。
また、再利用可能なスタイルモジュールを作成することで、コードの重複を減らし、開発効率を向上させることができます。

特徴まとめ④ 【柔軟なスタイル定義】 オブジェクト指向のスタイル記述

vanilla-extract は、オブジェクト指向のスタイル記述をサポートしています。
スタイルをオブジェクトとして定義し、プロパティをキーと値のペアで記述できるので、スタイルの定義がわかりやすく、スタイルのカスタマイズが容易になります。

vanilla-extractの基本的な使い方

続いて、vanilla-extract の基本的な使い方について説明します。
こちらの内容は公式に記載がありますので、読み飛ばしていただいて問題ありません。
https://vanilla-extract.style/documentation/getting-started/

インストールと設定方法の解説

まず、プロジェクトに vanilla-extract をインストールします。

npm install @vanilla-extract/css

次に、vanilla-extract を使用するための設定ファイルを作成します。
next.config.jsに以下の設定を追加します。

javascript
const withVanillaExtract = require('@vanilla-extract/next-plugin').withVanillaExtract;

module.exports = withVanillaExtract({
  reactStrictMode: true,
});

以上で設定が完了しました。

グローバルスタイルとローカルスタイルの管理方法

vanilla-extract では、グローバルスタイルとローカルスタイルを管理するための機能があります。
グローバルスタイルは、プロジェクト全体で使用されるスタイルであり、ローカルスタイルは、特定のコンポーネントにのみ適用されるスタイルです。

グローバルスタイルを定義するには、`globalStyle関数を使用します。

import { globalStyle } from '@vanilla-extract/css';

globalStyle('body', {
  backgroundColor: 'gray',
  fontSize: '16px',
});

ローカルスタイルを定義するには、style関数を使用します。

import { style } from '@vanilla-extract/css';

export const localStyle = style({
  backgroundColor: 'blue',
  color: 'white',
  padding: '10px',
});

CSSのモジュール化と型付け

vanilla-extract は、CSSをモジュール化し、TypeScriptによる型付けを可能にすることで、スタイルの管理と開発効率を大幅に向上させます。

style.css.tsファイルの作成と利用方法

vanilla-extract では、CSSのモジュール化のために、style.css.tsファイルを作成し、その中でスタイル定義します。

例えば、以下のようにボタンのスタイルを定義します。

styles.css.ts
import { style } from '@vanilla-extract/css';

export const button = style({
  padding: '20px',
  backgroundColor: 'lightblue',
});

次に、定義したスタイルをコンポーネントに適用します。

app/components/Button.tsx
import * as styles from './styles.css';

export function Button() {
  return (
    <button className={button}>
        vanilla-extract CSSを使用したボタン
    </button>
  );
};

型定義の自動生成とReactコンポーネントでの利用

vanilla-extract は、スタイル定義に基づいて自動的に型定義を生成します。

style.css.ts
import { style } from 'vanilla-extract';

export const buttonStyle = style({
  backgroundColor: 'blue',
  color: 'white',
  padding: '10px 20px',
});

上記のコードから、buttonStyleの型定義が自動的に生成されます。

style.css.ts
export const buttonStyle: {
  className: string;
}

この型定義は、Reactコンポーネントで利用することができます。

ExampleComponent.tsx
import { buttonStyle } from './style.css.ts';

const ExampleComponent = () => {
  return (
    <button className={buttonStyle.className}> 
        vanilla-extract CSSを使用したボタン
    </button>
  );
};

vanilla-extract を使用すると、スタイル定義はTypeScriptの型システムによって管理されるため、型安全性が向上します。これにより、スタイルの誤りをコンパイル時に検出することができ、開発体験が向上します。

CSSのモジュール化によるスタイルの管理と再利用性の向上

vanilla-extract は、CSSのモジュール化を強力にサポートすることで、スタイルの管理と再利用性を飛躍的に向上させます。

従来のCSSでは、グローバルな名前空間でスタイルを定義するため、異なるコンポーネントやライブラリ間でスタイル名が衝突する可能性がありました。

しかし、vanilla-extract では、各スタイル定義がローカルスコープにカプセル化されるため、他のモジュールと干渉することがなく、スタイルの衝突による意図しない動作などを回避できます。

さらに、vanilla-extract は、共通スタイルをモジュールとして定義し、他のコンポーネントでインポートすることを容易にすることで、コードの重複を減らし、保守性を向上させます。
共通スタイルを修正する際にも、一箇所変更するだけで、すべての関連コンポーネントに反映されるため、開発効率も大幅に向上します。

例えば、ボタンのスタイルを定義したbutton.css.tsモジュールと、カードのスタイルを定義したcard.css.tsモジュールを作成し、それぞれを別のコンポーネントでインポートして利用できます。

button.css.ts
import { style } from 'vanilla-extract';

export const buttonStyle = style({
  backgroundColor: 'blue',
  color: 'white',
  padding: '10px 20px',
  borderRadius: '4px',
  cursor: 'pointer',
});
card.css.ts
import { style } from 'vanilla-extract';

export const cardStyle = style({
  backgroundColor: 'white',
  padding: '20px',
  borderRadius: '8px',

それぞれをimportすることで使用できる。

ExampleComponent.css.ts
import { buttonStyle } from './button.css.ts';
import { cardStyle } from './card.css.ts';

const ExampleComponent = () => {
  return (
    <div className={cardStyle}>
      <button className={buttonStyle}>Click me</button>
    </div>
  );
};

パフォーマンスと開発体験の向上

vanilla-extract は、Zero-Runtimeと型安全性のメリットに加えて、開発者のパフォーマンスと開発体験を向上させるための機能も提供しています。

  • 【バンドルサイズの最適化】 不要なコードの削除とTree shaking
  • 【開発時のホットリロード】 スタイルの変更をリアルタイムに反映
  • 【デバッグとエラー処理】 分かりやすいエラーメッセージとソースマップの活用

vanilla-extract と CSS Modules の比較

【類似点】 CSSのモジュール化実現と型安全性の確保

どちらもCSSをモジュール化することで、スタイルの衝突を防ぎ、再利用性を高めます。
各コンポーネントに独自のスタイルスコープを提供し、グローバルな名前空間の汚染を防ぎます。

また、どちらもTypeScriptとの統合により、スタイル定義に型チェックを導入することで、コンパイル時にスタイルの誤りを検出できます。
型安全性を確保することで、開発中のバグを減らし、コードの信頼性を高めます。

【相違点】 Runtimeの有無とスタイル定義の方法

1. Runtimeの有無

【vanilla-extract】
Build時にCSSを生成するZero-Runtime方式を採用しています。
実行時にJavaScriptコードを実行する必要がないため、パフォーマンスに優れ、バンドルサイズを小さく抑えられます。

【CSS Modules】
Runtime(実行)時にCSSを生成します。
スタイル定義はJavaScriptコードで記述され、Runtime時にCSSに変換されます。

2. スタイル定義の方法

【vanilla-extract】
style関数を使用して、オブジェクト指向のスタイル定義を行います。

【CSS Modules」】
CSSファイル内でクラス名を定義し、JavaScriptコードからインポートして使用します。CSSの構文に則ったスタイル定義を行います。

【それぞれの長所と短所】 用途に応じた使い分けの提案

vanilla-extract の長所と短所

使い分けの提案

【vanilla-extract が適している場合】

  • パフォーマンス重視のプロジェクト: 高負荷なアプリケーションやパフォーマンスを最大限に引き出す必要があるプロジェクト。Zero-Runtimeにより、実行時のオーバーヘッドを排除し、高速なレンダリングを実現します。
  • 開発効率の重視のプロジェクト
    開発効率を高め、コードの信頼性を向上させたい場合。
    TypeScriptとの統合により、開発中にスタイルの誤りを早期に検出できます。

【CSS Modulesが適している場合】

  • シンプルなスタイル定義で、迅速な開発を重視するアプリケーション
  • デザイナーとの協業が多く、CSSの直接編集が頻繁に行われる場合。

ベストプラクティスとTips

Tips① 【ファイル構成の推奨パターン】 コンポーネントとスタイルの分離

※ こちら、弊社のプロジェクトでも採用しております。

vanilla-extract では、コンポーネントとスタイルを分離することで、コードの整理と保守性を向上させることができます。

各コンポーネントは、独自のファイルに記述します。
各コンポーネントのスタイルは、対応するコンポーネントファイルと同じディレクトリに、style.css.tsファイルとして作成します。

src/
  ├── components/
  │   ├── Button/
  │   │   └── Button.tsx
  │   │   └── style.css.ts
  │   └── Card/
  │       └── Card.tsx
  │       └── style.css.ts

Tips② 再利用可能なスタイルの抽出と共有方法

※ こちら、弊社のプロジェクトでも採用しております。
vanilla-extract では、共通のスタイルを抽出して、再利用可能なモジュールとして定義することができます。

まず、共通のスタイルをshared.css.tsのようなファイルに定義し、exportキーワードを使用して、スタイルを公開します。

shared.css.ts
import { style } from 'vanilla-extract';

export const buttonStyle = style({
  backgroundColor: 'blue',
  color: 'white',
  padding: '10px 20px',
  borderRadius: '4px',
  cursor: 'pointer',
});

他のコンポーネントファイルからshared.css.tsをインポートします。
インポートしたスタイルを、className属性に適用します。

ExampleComponent.tsx
import { buttonStyle } from './shared.css.ts';

const ExampleComponent = () => {
  return (
    <button className={buttonStyle}>Click me</button>
  );
};

再利用可能なスタイルをモジュール化することで、コードの重複を減らし、開発効率を向上させることができます。

Tips③ 【命名規則とスタイルの分離】 BEMなどの一貫した命名規則の採用

vanilla-extractでは、BEM(Block, Element, Modifier) のような一貫した命名規則を採用することで、スタイルの管理を容易にすることができます。

.button { /* Block */
  background-color: blue;
  color: white;
  padding: 10px 20px;
}

.button__icon { /* Element */
  margin-right: 5px;
}

.button--primary { /* Modifier */
  background-color: red;
}

まとめ

この記事では、私個人として、App Router時代のスタイリング戦略において最適な選択肢となる、CSS-in-JSライブラリ「vanilla-extract」について解説しました。

vanilla-extract は、パフォーマンス、型安全性、開発効率という三拍子が揃っています!

この記事を通して、 vanilla-extract の魅力が伝われば幸いです。
ぜひ、みなさんのプロジェクトでも導入を検討してみてください🧁

次回はemotionからvanilla-extract への移行や vanilla-extract のrecipeパッケージについて等もう少し深ぼった解説をさせていただきます。
https://emotion.sh/docs/introduction

最後までお読みいただきありがとうございました。

Discussion