🍦

Vanilla Extractを使ったスケーラブルなReactデザインシステムの実装

2024/12/13に公開

はじめに

モダンなフロントエンド開発において、よく構造化されたデザインシステムは一貫性、スケーラビリティ、メンテナビリティを保証します。特にReactプロジェクトでは、UIデザインの基盤となる重要な要素です。しかし、このシステムを手動で管理することは、冗長性やエラーを引き起こす原因となりがちです。

Vanilla Extractは、ゼロランタイムで動作するCSS-in-TypeScriptライブラリであり、この課題に対処するのに役立ちます。これにより、開発者はタイプセーフで再利用可能なスタイルを効率的に作成できます。

この記事では、Vanilla Extractを活用して、パレット、ステート、テーマを持つスケーラブルなボタンデザインシステムを作成する方法を紹介します。

シンプルなボタンデザインシステム

基本を理解するために、まずはボタン用のシンプルなデザインシステムを定義します。

パレット

パレット名 背景色 テキスト色 ボーダー色
プライマリ #007bff (青) #ffffff #0056b3
セカンダリ #6c757d (灰) #ffffff #5a6268
サクセス #28a745 (緑) #ffffff #218838
ダンジャー #dc3545 (赤) #ffffff #c82333

ステート

ステート 不透明度 ボーダースタイル その他の注意点
デフォルト 1.0 ソリッド 通常のボタンスタイル
ホバー 1.0 ソリッド 背景が少し暗くなる
無効 0.6 ダッシュ 不透明度が減少

テーマ

テーマ名 プライマリ背景 セカンダリ背景 テキスト色
ライト #f8f9fa (ライトグレー) #ffffff (白) #212529
ダーク #343a40 (ダークグレー) #212529 (黒) #f8f9fa

CSSへの変換の手間

上記のデザインシステムを手動でCSSに変換すると、各パレット、ステート、テーマに対応するクラスセレクタを作成する必要があります。例えば:

.btn-primary {
 background-color: #007bff;
 color: #ffffff;
 border-color: #0056b3;
}

.btn-primary:hover {
 background-color: #0056b3;
}

.btn-disabled {
 opacity: 0.6;
 border-style: dashed;
}

/* 他のパレットやステートについても同様 */

この繰り返しの作業は、システムが拡張するにつれて煩雑になり、エラーが発生しやすくなります。コンポーネント間でスタイルを一貫して管理し、更新することが難しくなります。

マスターデータに変換

CSSにスタイルをハードコードする代わりに、デザインシステムをJSON形式の構造化されたマスターデータとして定義することができます。例を見てみましょう:

const masterData = {
 palettes: {
   primary: {
     backgroundColor: "#007bff",
     textColor: "#ffffff",
     borderColor: "#0056b3",
   },
   secondary: {
     backgroundColor: "#6c757d",
     textColor: "#ffffff",
     borderColor: "#5a6268",
   },
 },
 states: {
   default: {
     opacity: 1.0,
     borderStyle: "solid",
   },
   hover: {
     opacity: 1.0,
     borderStyle: "solid",
     backgroundColorAdjustment: 0.1,
   },
 },
} as const;

このアプローチの利点:

  • スケーラビリティ: 新しいパレット、ステート、テーマを追加するのが簡単。
  • 一貫性: すべてのスタイルは1つの真実のソースから派生します。
  • 再利用性: 同じデータを複数のコンポーネントで利用できます。

良くないマスターデータ構造の例

不適切なマスターデータの構造例は次の通りです:

{
 "primaryBackgroundColor": "#007bff",
 "primaryTextColor": "#ffffff",
 "primaryBorderColor": "#0056b3",
 "secondaryBackgroundColor": "#6c757d",
 "secondaryTextColor": "#ffffff",
 "secondaryBorderColor": "#5a6268",
 "defaultOpacity": 1.0,
 "hoverOpacity": 1.0,
 "hoverBackgroundColorAdjustment": 0.1
}

欠点:

  • 繰り返しが多く、グルーピングが不足しているため、更新や拡張が難しい。
  • 階層が不明確で、値が誤って使用されるリスクがある。
  • コンポーネントロジックに直接マッピングするのが難しい。

マスターデータをタイプに変換

TypeScriptのkeyofを使用して、マスターデータから再利用可能なタイプを導出します:

type Palette = keyof typeof masterData["palettes"];
type State = keyof typeof masterData["states"];
type Theme = keyof typeof masterData["themes"];

このアプローチにより:

  • タイプセーフ: 定義されていないパレットやステートの使用を防止。
  • メンテナンスの容易さ: masterDataを変更した場合、タイプが自動的に更新されます。

これらのタイプを活用することで、コンポーネントのプロパティを検証したり、デザインシステムのコードベースで厳密な型チェックを実装したりできます。

基本的なReactコンポーネントの作成

まず、styleプロパティを使ってスタイルを動的に適用する基本的なReactコンポーネントを作成します。以下のように実装できます:

import React from "react";

interface ButtonProps {
 palette: Palette;
 state: State;
 theme: Theme;
}

const Button = ({ palette, state, theme }: Props) => {
 const styles = {
   ...masterData.palettes[palette],
   ...masterData.states[state],
   ...masterData.themes[theme],
 };

 return <button style={styles}>Click Me</button>;
};

解説:

  • stylesオブジェクトは、masterDataからpalettestatethemeの値をマージして動的に生成されます。
  • このアプローチはスタイルの柔軟性を示していますが、以下の制限があります:
    • 再利用性とスケーラビリティが制限される。
    • レンダリング時にスタイルの再計算が行われる。

Vanilla Extractの使用

インラインスタイルの制限を解決するために、Vanilla Extractを使用して再利用可能でタイプセーフなCSSクラスを作成します。これにより、パフォーマンスが向上し、メンテナンスが簡単になります。

以下のように実装できます:

import { style, styleVariants } from "@vanilla-extract/css";

const button = style({
  padding: "10px 20px",
  borderRadius: "4px",
});

const paletteVariants = styleVariants(masterData.palettes, (palette) => ({
  backgroundColor: palette.backgroundColor,
  color: palette.textColor,
  borderColor: palette.borderColor,
}));

const stateVariants = styleVariants(masterData.states, (state) => ({
  opacity: state.opacity,
  borderStyle: state.borderStyle,
}));

const themeVariants = styleVariants(masterData.themes, (theme) => ({
  backgroundColor: theme.primaryBackground,
  color: theme.textColor,
}));

export const buttonStyles = {
  button,
  paletteVariants,
  stateVariants,
  themeVariants,
};

主な改善点:

  • 静的CSSクラス: クラスはビルド時に生成され、ランタイムのオーバーヘッドを排除します。
  • スケーラビリティのためのバリアント: styleVariantsを使って、masterDataのキーを自動的にクラス名にマッピングします。
  • 責任の分離: パレットやステートはモジュール化されており、更新や拡張が容易です。

Reactコンポーネントでこれらのクラスを使用するように更新します:

import { buttonStyles } from "./styles.css";

const Button = ({ palette, state, theme }: { palette: Palette; state: State; theme: Theme }) => {
  return (
    <button
      className={`${buttonStyles.button} 
                  ${buttonStyles.paletteVariants[palette]} 
                  ${buttonStyles.stateVariants[state]} 
                  ${buttonStyles.themeVariants[theme]}`}
    >
      Click Me
    </button>
  );
};

なぜVanilla Extractが優れているのか

Vanilla Extractは、Reactプロジェクトでデザインシステムを実装するための魅力的な選択肢です。以下の利点があります:

  1. ゼロランタイムオーバーヘッド: 伝統的なCSS-in-JSソリューションとは異なり、Vanilla Extractはビルド時に静的なCSSを生成します。これにより、ランタイムでのスタイリングによるパフォーマンスのペナルティが排除され、アプリケーションのパフォーマンスが向上します。

  2. タイプセーフ: TypeScriptを活用することで、デザインシステムがタイプセーフになります。これにより、ランタイムではなくコンパイル時に誤ったパレットやステートを使用していることを検出できます。

  3. スケーラビリティ: styleVariantsを使ってバリアントを定義・管理できるため、デザインシステムを簡単にスケールできます。新しいテーマやパレット、ステートを追加するのが簡単です。

  4. メンテナンス性: スタイルとコンポーネントの責任が分離されているため、スタイルの更新やメンテナンスが容易です。静的なCSSクラスを使用することで、より整理されたコードベースになります。

  5. クリーンなコード: Vanilla Extractは宣言的なスタイル管理を可能にし、インラインでスタイルを管理するよりもクリーンで予測可能な構造を提供します。

Vanilla Extractを使うことで、パフォーマンスを最適化し、コードの複雑さを減らし、開発者の生産性を向上させることができます。


結論

この記事では、Vanilla Extractを使ってスケーラブルでメンテナブルなボタンデザインシステムを実装する方法を紹介しました。パレット、ステート、テーマのマスターデータを定義し、Vanilla ExtractのタイプセーフなゼロランタイムCSSを活用することで、柔軟で効率的なソリューションを作成しました。

Vanilla Extractは、パフォーマンス、メンテナンス性、スケーラビリティの観点から優れたツールであり、TypeScriptとの統合により、スタイルがタイプセーフになります。これにより、エラーを減らし、コードへの信頼性を高めることができます。

このアプローチを使えば、視覚的に一貫性があり、パフォーマンスが最適化されたReactアプリケーションを構築でき、デザインシステムが進化しても簡単にメンテナンスできます。

Discussion