404 Not Found

404 Not Found


nginx

Reactのスタイリングソリューション

大規模なECサイトのバックエンドプロジェクトの保守を担当していたトムは、チームの規模が拡大するにつれて、グローバルCSSにおけるクラス名の競合がますます頻繁に発生していることに気づきました。たった1つのボタンのスタイルを変更するだけで、3つの異なるページの表示に影響が及ぶこともありました。彼は、スタイル同士が干渉しないように「分離」できる解決策を必要としていました。


1. 学習内容


2. 概念図

以下の図は、「記述スタイル」と「実行時の挙動」という観点から、3つの設計アプローチ間の主な違いを示しています:

100%
flowchart LR
    A[React Design Concepts] --> B[CSS Modules]
    A --> C[Tailwind CSS]
    A --> D[styled-components]

    B --> B1["*.module.css Documents"]
    B --> B2["Compile to Generate a Unique Hash Class Name"]
    B --> B3["Natural Barrier / Zero Runtime"]

    C --> C1["Tool Sets className"]
    C --> C2["PurgeCSS Remove unused styles"]
    C --> C3["Build at Compile Time / Zero Runtime"]

    D --> D1["JS Styling Template Strings"]
    D --> D2["Runtime Injection style Tags"]
    D --> D3["Props Driving Dynamic Styles"]

    style B fill:#e3f2fd,stroke:#1565c0
    style C fill:#e8f5e9,stroke:#2e7d32
    style D fill:#fff3e0,stroke:#e65100

3. 実際の事例

スタイリングのアプローチ 分離 実行時のオーバーヘッド 動的スタイリング SSR との互換性
インラインスタイル 分離なし なし ✅ JSを直接記述
CSSモジュール ハッシュの分離 なし ❌ classNameの連結が必要
Tailwind CSS ミニフィケーションなし (PurgeCSS) なし ⚠️ 条件付きクラス名の組み合わせ
styled-components 一意のクラス名の分離 はい(実行時インジェクション) ✅ プロップ駆動型 ⚠️ SSRの設定が必要

トムのチームは、50ページを超えるバックエンドシステムを運用しています。当初、彼らはグローバルCSSファイルを使用しており、クラスの命名規則は完全にBEMに基づいていました。5人のフロントエンド開発者が同時にこのプロジェクトに携わるようになると、次のような問題が発生しました:

トムは、これらの課題に対処するため、エンジニアリング主導のスタイリング手法を採用することに決めました。彼はまずCSS Modulesを試してみて、次にTailwind CSSをテストし、最終的にはstyled-componentsを使用してデザインシステムを構築しました。ここでは、これら3つのアプローチすべてについて、その手順を詳しく解説します。


(1) CSSモジュール — コストゼロのスタイル分離

CSSモジュールは、Reactプロジェクトでスタイリングの分離を始めるのに最も手軽なソリューションです。各CSSファイルを、一意のハッシュ化されたクラス名を持つモジュールにコンパイルすることで、クラス名の競合を根本的に解消します。

基本原則:.button と記述すると、コンパイル時に .Button_button_abc123 に変換されるため、他のコンポーネントが誤ってこれにアクセスしてしまうことはありません。

CSSモジュールファイルの作成

CSS
/* Button.module.css */
.button {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.2s ease;
}

.primary {
  background: #1890ff;
  color: white;
}

.primary:hover {
  background: #40a9ff;
}

.danger {
  background: #ff4d4f;
  color: white;
}

.danger:hover {
  background: #ff7875;
}

.default {
  background: #f0f0f0;
  color: #333;
}

.default:hover {
  background: #d9d9d9;
}

コンポーネントへのインポートと使用

JSX
import styles from './Button.module.css'

function Button({ variant = 'primary', children, onClick }) {
  return (
    <button
      className={`${styles.button} ${styles[variant]}`}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

export default Button

コンパイルされたDOM出力:<button class="Button_button_1a2b3c Button_primary_4d5e6f">Submit</button>;このクラス名はグローバルに一意です。

動的なクラス名の連結に関するベストプラクティス

コンポーネントが複数の条件の組み合わせに基づいてクラス名を生成する必要がある場合、テンプレート文字列のネストが過度に深くなるのを避けるため、clsx または classnames ライブラリの使用をお勧めします:

BASH
npm install clsx
JSX
import styles from './Button.module.css'
import clsx from 'clsx'

function Button({ variant = 'primary', size = 'medium', disabled, children }) {
  return (
    <button
      className={clsx(
        styles.button,
        styles[variant],
        styles[size],
        { [styles.disabled]: disabled }
      )}
      disabled={disabled}
    >
      {children}
    </button>
  )
}
CSS
/* Button.module.css — Add New Size Variants */
.small { padding: 4px 12px; font-size: 12px; }
.medium { padding: 8px 20px; font-size: 14px; }
.large { padding: 12px 28px; font-size: 16px; }
.disabled { opacity: 0.5; cursor: not-allowed; }

(1) ▶ サンプル:CSS モジュールで複数のクラス名を組み合わせる

JSX
import styles from './Card.module.css'

function Card({ title, children, isHighlighted }) {
  return (
    <div
      className={
        `${styles.card} ${isHighlighted ? styles.highlighted : ''}`
      }
    >
      <h3 className={styles.title}>{title}</h3>
      <div className={styles.content}>{children}</div>
    </div>
  )
}
▶ 試してみよう
CSS
/* Card.module.css */
.card {
  border: 1px solid #e8e8e8;
  border-radius: 8px;
  padding: 16px;
  background: #fff;
}

.highlighted {
  border-color: #1890ff;
  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}

.title {
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 8px;
  color: #1a1a1a;
}

.content {
  font-size: 14px;
  color: #666;
  line-height: 1.6;
}

適したシナリオ: 中小規模のプロジェクト、新メンバーが多いチーム、可能な限り低コストでスタイルの分離を図りたいプロジェクト。


(2) Tailwind CSS — アトミックな設計、迅速な開発

Tailwind CSS は、アトミック(ユーティリティファースト)な CSS クラス名のセットを提供しています。開発者はカスタム CSS を記述する必要がなく、代わりに JSX 内でアトミッククラスを組み合わせて UI を構築します。

重要なポイント:クラスに名前を付ける代わりに、p-4(padding: 16px)、text-lg(font-size: 18px)、bg-blue-500 など、意味が明確なユーティリティクラスを使用しましょう。

インストールと設定

BASH
npm install -D tailwindcss @tailwindcss/vite
JS
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})
CSS
/* index.css */
@import "tailwindcss";

(2) ▶ サンプル:Tailwind CSS を使ったカードコンポーネントの作成

JSX
function ProductCard({ product, onAddToCart }) {
  return (
    <div className="border border-gray-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow bg-white">
      <img
        src={product.image}
        alt={product.name}
        className="w-full h-48 object-cover rounded-md mb-3"
      />
      <h3 className="text-lg font-semibold text-gray-800 mb-1">
        {product.name}
      </h3>
      <p className="text-sm text-gray-500 mb-2 line-clamp-2">
        {product.description}
      </p>
      <div className="flex items-center justify-between">
        <span className="text-xl font-bold text-blue-600">
          ${product.price}
        </span>
        <button
          onClick={() => onAddToCart(product)}
          className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
        >
          Add to Cart
        </button>
      </div>
    </div>
  )
}
▶ 試してみよう

Tailwindのメリット:

活用シーン: ラピッドプロトタイピング、スタートアッププロジェクト、およびチームが統一されたアトミックデザインのアプローチを採用する中規模から大規模のプロジェクト。


(3) styled-components — CSS-in-JS デザインシステム

styled-components は、CSS-in-JS の代表的な実装の一つであり、JavaScript 内のテンプレート文字列を使用してスタイルを記述し、そのスタイルはコンポーネントと密接に結びついています。

インストール

BASH
npm install styled-components

(3) ▶ サンプル:styled-components のテーマ別ボタンシステム

JSX
import styled, { ThemeProvider } from 'styled-components'

// Define the Topic
const theme = {
  colors: {
    primary: '#1890ff',
    success: '#52c41a',
    danger: '#ff4d4f',
    text: '#333',
    textLight: '#fff',
  },
  radii: {
    sm: '4px',
    md: '8px',
    lg: '12px',
  },
  fonts: {
    body: "'Inter', sans-serif",
  },
}

// Styled Components — Automatic Receipt theme prop
const StyledButton = styled.button`
  padding: 10px 24px;
  border: none;
  border-radius: ${props => props.theme.radii.md};
  font-family: ${props => props.theme.fonts.body};
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;

  /* props Conditional Styles Driven by Rules */
  background: ${props => {
    if (props.$variant === 'danger') return props.theme.colors.danger
    if (props.$variant === 'success') return props.theme.colors.success
    return props.theme.colors.primary
  }};
  color: ${props => props.theme.colors.textLight};

  &:hover {
    opacity: 0.85;
    transform: translateY(-1px);
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
  }
`

const ButtonGroup = styled.div`
  display: flex;
  gap: 12px;
  padding: 20px;
`

function App() {
  return (
    <ThemeProvider theme={theme}>
      <ButtonGroup>
        <StyledButton $variant="primary">Main Buttons</StyledButton>
        <StyledButton $variant="success">Success Button</StyledButton>
        <StyledButton $variant="danger" disabled>Danger(Disable)</StyledButton>
      </ButtonGroup>
    </ThemeProvider>
  )
}
▶ 試してみよう

styled-components の主な機能:

活用例: 大規模なデザインシステム、動的なテーマ切り替えが必要なシナリオ、およびUIコンポーネントライブラリの開発。


4. 解決策の比較

ディメンション CSSモジュール Tailwind CSS styled-components
習得の難易度 低(標準的なCSS) 中(クラス名の暗記が必要) 中(CSS-in-JSの理解が必要)
スタイルの分離 完全分離(ハッシュ) なし(グローバルクラス名) 完全分離(一意のクラス名)
Runtime overhead None None Yes (injects a style tag)
Dynamic Styles Concatenating via JS className Conditional Class Name Combination Directly Driven by Props
テーマシステム 追加のソリューションが必要 CSS変数 + 設定 ThemeProviderに組み込まれている
ビルド出力 CSSファイルを分離 PurgeCSS処理後の最小サイズ JSバンドルに含める
最適なプロジェクト規模 小~中規模 中~大規模 大規模/デザインシステム

5. モデル選択に関する推奨事項

チーム/プロジェクトの詳細 推奨される解決策
2~5人のチーム、中小規模のプロジェクト CSSモジュール(分離を実現する最も費用対効果の高い方法)
スタートアップチームには迅速な反復開発が必要 Tailwind CSS(CSSを記述する必要がなく、開発がスピードアップ)
大規模なエンタープライズプロジェクト、システム設計 styled-components(包括的なテーマシステム)
既存プロジェクトの移行 現在のアプローチを維持しつつ、新しいモジュールを徐々にCSSモジュールに置き換えていく
コンポーネントライブラリの開発 styled-components または CSS Modules(どちらでも可)

(1) ▶ サンプル:CSS モジュールによるコンポーネントのスタイル設定

JSX
// Button.module.css
// .primary { background: #1677ff; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; }
// .danger { background: #ff4d4f; color: white; }
// .outline { background: transparent; border: 1px solid #1677ff; color: #1677ff; }
// .small { padding: 4px 8px; font-size: 12px; }
// .disabled { opacity: 0.5; cursor: not-allowed; }

import styles from './Button.module.css'

function Button({ variant = 'primary', size, disabled, children, ...props }) {
  const classNames = [
    styles[variant],
    size === 'sm' && styles.small,
    disabled && styles.disabled,
  ].filter(Boolean).join(' ')

  return (
    <button className={classNames} disabled={disabled} {...props}>
      {children}
    </button>
  )
}

function ButtonDemo() {
  return (
    <div style={{ display: 'flex', gap: 8 }}>
      <Button>Primary</Button>
      <Button variant="danger">Delete</Button>
      <Button variant="outline" size="sm">Small Outline</Button>
      <Button disabled>Disabled</Button>
    </div>
  )
}
▶ 試してみよう

(2) ▶ サンプル:Tailwind CSS を使ったレスポンシブなカードグリッド

TSX
function ProductGrid({ products }) {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-4">
      {products.map(product => (
        <div key={product.id} className="bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 overflow-hidden">
          <div className="h-48 bg-gray-200 flex items-center justify-center">
            <span className="text-gray-400 text-4xl">📦</span>
          </div>
          <div className="p-4">
            <h3 className="font-semibold text-gray-800 truncate">{product.name}</h3>
            <p className="text-sm text-gray-500 mt-1">{product.category}</p>
            <div className="flex items-center justify-between mt-3">
              <span className="text-lg font-bold text-red-500">${product.price}</span>
              <button className="bg-blue-500 hover:bg-blue-600 text-white text-sm px-3 py-1 rounded transition-colors">
                Add to Cart
              </button>
            </div>
          </div>
        </div>
      ))}
    </div>
  )
}
▶ 試してみよう

❓ よくある質問

Q CSS ModulesとCSS-in-JS、どちらのパフォーマンスが優れていますか?
A CSS Modulesは、ビルドプロセス中に静的なCSSファイルを生成し、実行時のオーバーヘッドを排除するため、パフォーマンスに優れています。一方、Styled-Componentsはレンダリング時にstyleタグをDOMに挿入するため、初回レンダリング時にわずかなオーバーヘッドが発生します。ほとんどのアプリケーションではその差は無視できる程度ですが、パフォーマンスが重要なアニメーションのシナリオでは、CSS Modulesの方が有利です。
Q Tailwind CSSのクラス名が長すぎてコードの可読性に影響する場合、どうすればよいですか?
A @applyディレクティブを使用して複数のユーティリティクラスをカスタムCSSクラスに抽出するか、clsxライブラリを使って条件付き連結を行うことができます。また、ほとんどのエディタ(Tailwind CSSのIntelliSenseを備えたVS Codeなど)では、クラス名の自動補完やホバープレビュー機能が提供されているため、実際の開発においてクラス名の可読性が問題になることはありません。
Q styled-components の $variant$ というプレフィックスが付いているのはなぜですか?
A $ というプレフィックスは、styled-components v5.2 以降で導入された「一時プロパティ(transient prop)」の規約の一部であり、このプロパティがスタイルの計算にのみ使用され、下位の DOM 要素には渡されないことを示しています。$というプレフィックスがない場合、そのプロパティはHTML属性(例:<button variant="primary">)としてレンダリングされ、コンソールに警告が表示されます。
Q 同じReactプロジェクト内で、複数のスタイリングソリューションを組み合わせて使用することはできますか?
A はい、可能ですし、かなり一般的な手法です。推奨されるアプローチは、グローバルレイアウトにはTailwind CSS(フレームワークの迅速なセットアップのため)、ビジネスコンポーネントにはCSS Modules(安定性と信頼性のため)、デザインシステムのコアコンポーネントにはstyled-components(柔軟なテーマ設定のため)を使用することです。これら3つのソリューションは、それぞれ異なるコンパイルメカニズムを使用しているため、互いに競合することはありません。
Q CSS-in-JSには実行時のオーバーヘッドがかなりありますか?パフォーマンスに影響しますか?
A 実行時のCSS-in-JS(styled-componentsなど)には確かにある程度のオーバーヘッドがあります。コンポーネントがレンダリングされるたびに、classNamesが生成され、<style>タグが挿入されるためです。しかし、実際の影響は通常、無視できる程度です。その理由は以下の通りです:① React 18の並行レンダリングにより、レンダリングのブロックはすでに軽減されています;② パフォーマンスのボトルネックのほとんどは、スタイルの計算ではなくJavaScriptのロジックに起因しています;③ 万が一パフォーマンスの問題が発生した場合は、ランタイムオーバーヘッドのないソリューション(vanilla-extractやPanda CSSなど)に切り替えることができます。

📖 まとめ


📝 練習問題

  1. CSS Modules を使用して Avatar コンポーネントを作成します。このコンポーネントは、size(small/medium/large)と shape(circle/square)の 2 つのプロパティをサポートしており、3 つのサイズはそれぞれ異なる幅と高さに相当します。
  2. レスポンシブデザインを考慮して、上記のAvatarコンポーネントをTailwind CSSを使用して書き直してください。モバイル端末ではsmallを使用し、デスクトップでは自動的にmediumに切り替わるようにしてください。
  3. styled-components を使用して、Badge プロパティを通じて表示される数値を制御する Badge コンポーネントを作成します。数値が 99 を超える場合は「99+」と表示し、theme.colors プロパティを使用して色を変化させます。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%