Reactでスタイルを当てる3つの方法|CSS・インライン・CSS Modules

Reactでのスタイリング手法を初心者向けに詳しく解説。CSS、インライン、CSS Modulesの使い方から使い分けのポイントまで、実践的なサンプルコードとともに紹介します。

Learning Next 運営
68 分で読めます

Reactでスタイルを当てる3つの方法|CSS・インライン・CSS Modules

みなさん、Reactアプリを作る時に「スタイルはどうやって書けばいいの?」と迷ったことはありませんか?

「CSSファイルとインラインスタイルどちらがいいの?」「CSS Modulesって何?」と疑問に思ったことはありませんか?

React開発において、スタイリングは見た目を決める重要な要素です。 しかし、複数の方法があるため、どれを選べばいいか初心者には判断が難しいのが現実です。

この記事では、Reactでのスタイリング手法を初心者向けに詳しく解説します。 CSS、インライン、CSS Modulesの使い方から使い分けのポイントまで、実践的なサンプルコードとともに学んでいきましょう。

Reactでのスタイリング概要

3つの主要な方法

Reactでスタイルを適用する代表的な方法を見てみましょう。

各方法の特徴

まず、3つの方法を簡単に比較してみます。

外部CSSファイルは、別ファイルでCSSを書いてimportする方法です。 慣れ親しんだCSS記法で書けて、メディアクエリも使えます。 ただし、グローバルスコープなのでクラス名の重複に注意が必要です。

インラインスタイルは、JSX内にstyle属性で直接書く方法です。 コンポーネントスコープで安全で、動的スタイルが簡単に書けます。 でも、疑似クラスやメディアクエリは使えません。

CSS Modulesは、ローカルスコープのCSSファイルです。 CSS全機能が使えて、クラス名の重複も回避できます。 ただし、設定が必要で学習コストがかかります。

どの方法を選ぶべきか

プロジェクトの特性に応じた選択指針をご紹介します。

小規模プロジェクトでは、CSS + インラインの組み合わせがおすすめです。 シンプルで学習コストが低く、すぐに始められます。

中規模プロジェクトでは、CSS Modulesが最適です。 スコープ管理とメンテナンス性のバランスが良いからです。

大規模プロジェクトでは、CSS Modules + styled-componentsの組み合わせを推奨します。 高い保守性とコンポーネント指向の開発ができます。

方法1: 外部CSSファイル

基本的な使い方

最も一般的なCSS ファイルを使ったスタイリング方法を見てみましょう。

CSSファイルの作成と読み込み

/* src/App.css */
.app {
  text-align: center;
  padding: 20px;
  font-family: Arial, sans-serif;
}

.header {
  background-color: #282c34;
  padding: 20px;
  color: white;
  margin-bottom: 20px;
}

.header h1 {
  margin: 0;
  font-size: 2.5rem;
}

.main-content {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 20px;
  margin: 20px 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: box-shadow 0.3s ease;
}

.card:hover {
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}

.button {
  background-color: #007bff;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  transition: background-color 0.3s ease;
}

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

.button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

/* レスポンシブデザイン */
@media (max-width: 768px) {
  .main-content {
    padding: 10px;
  }
  
  .card {
    margin: 10px 0;
    padding: 15px;
  }
  
  .header h1 {
    font-size: 2rem;
  }
}

このCSSファイルは、基本的なレイアウトとスタイルを定義しています。

.appクラスでアプリ全体の設定を行います。 .headerでヘッダー部分のスタイルを指定し、.main-contentでメインエリアのレイアウトを決めています。

.cardクラスでは、カード型のUIコンポーネントを作成しています。 ホバー効果も追加して、マウスを乗せると影が濃くなります。

ボタンのスタイルも詳細に設定しています。 通常時、ホバー時、無効時のそれぞれの見た目を定義しています。

最後にメディアクエリでレスポンシブ対応を行っています。 768px以下の画面サイズでは、パディングやフォントサイズを調整します。

// src/App.js
import React, { useState } from 'react';
import './App.css'; // CSSファイルをインポート

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div className="app">
      <header className="header">
        <h1>React CSS スタイリング</h1>
      </header>
      
      <main className="main-content">
        <div className="card">
          <h2>カウンターアプリ</h2>
          <p>現在の値: <strong>{count}</strong></p>
          <button 
            className="button"
            onClick={() => setCount(count + 1)}
          >
            増やす
          </button>
          <button 
            className="button"
            onClick={() => setCount(count - 1)}
            style={{ marginLeft: '10px' }}
          >
            減らす
          </button>
          <button 
            className="button"
            onClick={() => setCount(0)}
            style={{ marginLeft: '10px' }}
          >
            リセット
          </button>
        </div>
        
        <div className="card">
          <h2>CSS ファイルの特徴</h2>
          <ul>
            <li>慣れ親しんだCSS記法が使える</li>
            <li>疑似クラス(:hover, :focus等)が使える</li>
            <li>メディアクエリでレスポンシブ対応</li>
            <li>CSS プリプロセッサ(Sass/Less)が使える</li>
          </ul>
        </div>
      </main>
    </div>
  );
};

export default App;

Reactコンポーネントでの使用例です。

まず、CSSファイルをインポートしています。 import './App.css';の行で、先ほど作成したCSSファイルを読み込みます。

className属性を使って、CSSのクラスを適用します。 HTMLのclassではなく、ReactではclassNameを使うことに注意してください。

一部のボタンでは、インラインスタイルも併用しています。 style={{ marginLeft: '10px' }}のように、細かい調整はインラインで行うのも良い方法です。

コンポーネント別CSSファイル

/* src/components/UserCard.css */
.user-card {
  display: flex;
  align-items: center;
  padding: 15px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  margin: 10px 0;
  background-color: #fff;
  transition: all 0.3s ease;
}

.user-card:hover {
  border-color: #007bff;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
}

.user-avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  margin-right: 15px;
  object-fit: cover;
  border: 2px solid #f0f0f0;
}

.user-info {
  flex: 1;
}

.user-name {
  margin: 0 0 5px 0;
  font-size: 1.2rem;
  font-weight: 600;
  color: #333;
}

.user-email {
  margin: 0 0 5px 0;
  color: #666;
  font-size: 0.9rem;
}

.user-role {
  display: inline-block;
  padding: 2px 8px;
  font-size: 0.8rem;
  border-radius: 12px;
  font-weight: 500;
}

.user-role--admin {
  background-color: #ff6b6b;
  color: white;
}

.user-role--user {
  background-color: #4ecdc4;
  color: white;
}

.user-role--guest {
  background-color: #95a5a6;
  color: white;
}

より実践的なユーザーカードコンポーネントのCSSです。

Flexboxレイアウトを使って、アバター画像と情報を横並びに配置します。 ホバー効果では、境界線の色を変えて、カードを少し浮き上がらせています。

アバター画像は円形に表示し、適切なサイズに調整しています。 object-fit: cover;で画像の縦横比を保ったままトリミングします。

ユーザーロールの表示では、BEM記法を使っています。 .user-role--adminのように、ベースクラスにModifierを追加する方法です。

// src/components/UserCard.js
import React from 'react';
import './UserCard.css';

const UserCard = ({ user }) => {
  const { name, email, role, avatar } = user;

  return (
    <div className="user-card">
      <img 
        src={avatar || 'https://via.placeholder.com/60'} 
        alt={`${name}のアバター`}
        className="user-avatar"
      />
      <div className="user-info">
        <h3 className="user-name">{name}</h3>
        <p className="user-email">{email}</p>
        <span className={`user-role user-role--${role}`}>
          {role === 'admin' ? '管理者' : role === 'user' ? 'ユーザー' : 'ゲスト'}
        </span>
      </div>
    </div>
  );
};

export default UserCard;

ユーザーカードコンポーネントの実装です。

分割代入でpropsから必要な値を取り出しています。 アバター画像がない場合は、プレースホルダー画像を表示します。

動的クラス名の生成も行っています。 `user-role user-role--${role}`のように、テンプレートリテラルでクラス名を組み立てます。

// 使用例
const UserList = () => {
  const users = [
    {
      id: 1,
      name: '田中太郎',
      email: 'tanaka@example.com',
      role: 'admin',
      avatar: 'https://via.placeholder.com/60/007bff/fff'
    },
    {
      id: 2,
      name: '佐藤花子',
      email: 'sato@example.com',
      role: 'user',
      avatar: 'https://via.placeholder.com/60/28a745/fff'
    }
  ];

  return (
    <div>
      <h2>ユーザー一覧</h2>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
};

ユーザーリストコンポーネントでの使用例です。

配列のmap関数を使って、ユーザーデータを繰り返し表示しています。 各ユーザーカードには、必ずkey属性を設定することが重要です。

CSSファイルの注意点

CSSファイルを使う際の注意点を理解しておきましょう。

グローバルスコープの問題があります。 すべてのクラス名がグローバルに適用されるため、異なるコンポーネントで同じクラス名を使うと競合が発生します。

例えば、Header.cssで.title { color: red; }と定義し、Footer.cssで.title { color: blue; }と定義すると、後から読み込まれた方で上書きされてしまいます。

解決策として、BEMやプレフィックスでの命名規則統一があります。

未使用CSSの蓄積も問題になりがちです。 コンポーネントを削除してもCSSが残っていると、バンドルサイズが増大します。 PurgeCSSや定期的なクリーンアップで対策しましょう。

CSS と JSX の分離による保守性の課題もあります。 ファイル命名規則の統一やコメントの充実で対応します。

BEM記法の例をご紹介します。

/* BEM (Block Element Modifier) 記法 */
.user-card { /* Block */ }
.user-card__avatar { /* Element */ }
.user-card__name { /* Element */ }
.user-card--featured { /* Modifier */ }
.user-card--disabled { /* Modifier */ }

BEMを使うことで、クラス名の意味が明確になり、コンポーネント間の競合を避けられます。

方法2: インラインスタイル

基本的な使い方

JSX内でstyle属性を使ったスタイリング方法を見てみましょう。

インラインスタイルの基本

// インラインスタイルの基本例
import React, { useState } from 'react';

const InlineStyleExample = () => {
  const [isActive, setIsActive] = useState(false);
  const [theme, setTheme] = useState('light');

  // スタイルオブジェクトを定義
  const containerStyle = {
    maxWidth: '600px',
    margin: '0 auto',
    padding: '20px',
    fontFamily: 'Arial, sans-serif',
    backgroundColor: theme === 'light' ? '#ffffff' : '#333333',
    color: theme === 'light' ? '#333333' : '#ffffff',
    transition: 'all 0.3s ease'
  };

  const headerStyle = {
    textAlign: 'center',
    marginBottom: '30px',
    padding: '20px',
    backgroundColor: theme === 'light' ? '#f8f9fa' : '#444444',
    borderRadius: '8px',
    border: `2px solid ${theme === 'light' ? '#e9ecef' : '#555555'}`
  };

  const buttonStyle = {
    backgroundColor: isActive ? '#28a745' : '#007bff',
    color: 'white',
    border: 'none',
    padding: '12px 24px',
    margin: '5px',
    borderRadius: '6px',
    cursor: 'pointer',
    fontSize: '16px',
    fontWeight: '500',
    transition: 'all 0.3s ease',
    transform: isActive ? 'scale(1.05)' : 'scale(1)',
    boxShadow: isActive ? '0 4px 8px rgba(0, 0, 0, 0.2)' : '0 2px 4px rgba(0, 0, 0, 0.1)'
  };

  const cardStyle = {
    border: '1px solid #ddd',
    borderRadius: '8px',
    padding: '20px',
    margin: '20px 0',
    backgroundColor: theme === 'light' ? '#ffffff' : '#444444',
    boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)'
  };

  return (
    <div style={containerStyle}>
      <header style={headerStyle}>
        <h1 style={{ margin: 0, color: theme === 'light' ? '#333' : '#fff' }}>
          インラインスタイル デモ
        </h1>
      </header>

      <div style={cardStyle}>
        <h2>動的スタイリング</h2>
        <p>ボタンの状態とテーマに応じてスタイルが変化します。</p>
        
        <button
          style={buttonStyle}
          onClick={() => setIsActive(!isActive)}
          onMouseEnter={(e) => {
            e.target.style.opacity = '0.8';
          }}
          onMouseLeave={(e) => {
            e.target.style.opacity = '1';
          }}
        >
          {isActive ? 'アクティブ' : '非アクティブ'}
        </button>

        <button
          style={{
            ...buttonStyle,
            backgroundColor: theme === 'light' ? '#6c757d' : '#ffc107',
            color: theme === 'light' ? 'white' : 'black'
          }}
          onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
        >
          テーマ切替: {theme === 'light' ? 'ライト' : 'ダーク'}
        </button>
      </div>

      <div style={cardStyle}>
        <h3>現在の状態</h3>
        <ul style={{ paddingLeft: '20px' }}>
          <li>アクティブ状態: {isActive ? 'ON' : 'OFF'}</li>
          <li>テーマ: {theme === 'light' ? 'ライトモード' : 'ダークモード'}</li>
          <li>
            背景色: 
            <span style={{
              display: 'inline-block',
              width: '20px',
              height: '20px',
              backgroundColor: containerStyle.backgroundColor,
              border: '1px solid #ccc',
              marginLeft: '10px',
              verticalAlign: 'middle'
            }} />
          </li>
        </ul>
      </div>
    </div>
  );
};

export default InlineStyleExample;

インラインスタイルの基本的な使い方を見てみましょう。

スタイルオブジェクトを定義して、style属性に渡します。 CSSプロパティ名は、ケバブケース(background-color)ではなくキャメルケース(backgroundColor)で書きます。

動的なスタイルが簡単に実装できます。 themeisActiveの状態に応じて、色や形を変更しています。

条件演算子を使って、状態に応じたスタイルを切り替えています。 例えば、backgroundColor: theme === 'light' ? '#ffffff' : '#333333'のように書きます。

スプレッド演算子も活用できます。 ...buttonStyleでベースのスタイルを継承し、一部のプロパティだけを上書きしています。

イベントハンドラー内でのスタイル変更も可能です。 onMouseEnteronMouseLeaveで、ホバー効果を実装しています。

プロップスに基づく動的スタイル

// プロップスに基づくスタイリング
const DynamicButton = ({ variant, size, disabled, children, onClick }) => {
  // バリアント別の色設定
  const variants = {
    primary: { backgroundColor: '#007bff', color: 'white' },
    secondary: { backgroundColor: '#6c757d', color: 'white' },
    success: { backgroundColor: '#28a745', color: 'white' },
    danger: { backgroundColor: '#dc3545', color: 'white' },
    warning: { backgroundColor: '#ffc107', color: 'black' },
    info: { backgroundColor: '#17a2b8', color: 'white' },
    light: { backgroundColor: '#f8f9fa', color: 'black', border: '1px solid #ddd' },
    dark: { backgroundColor: '#343a40', color: 'white' }
  };

  // サイズ別の設定
  const sizes = {
    small: { padding: '6px 12px', fontSize: '14px' },
    medium: { padding: '10px 20px', fontSize: '16px' },
    large: { padding: '14px 28px', fontSize: '18px' }
  };

  // 基本スタイル
  const baseStyle = {
    border: 'none',
    borderRadius: '4px',
    cursor: disabled ? 'not-allowed' : 'pointer',
    fontWeight: '500',
    transition: 'all 0.3s ease',
    opacity: disabled ? 0.6 : 1,
    outline: 'none',
    textDecoration: 'none',
    display: 'inline-block',
    textAlign: 'center',
    lineHeight: '1.5',
    ...variants[variant || 'primary'],
    ...sizes[size || 'medium']
  };

  return (
    <button
      style={baseStyle}
      onClick={disabled ? undefined : onClick}
      disabled={disabled}
      onMouseEnter={!disabled ? (e) => {
        e.target.style.filter = 'brightness(0.9)';
        e.target.style.transform = 'translateY(-1px)';
      } : undefined}
      onMouseLeave={!disabled ? (e) => {
        e.target.style.filter = 'brightness(1)';
        e.target.style.transform = 'translateY(0)';
      } : undefined}
    >
      {children}
    </button>
  );
};

再利用可能なボタンコンポーネントの実装例です。

**バリアント(variant)**でボタンの色を指定できます。 primary、secondary、successなど、目的に応じた色設定を用意しています。

**サイズ(size)**でボタンの大きさを調整できます。 small、medium、largeの3段階で、パディングとフォントサイズを変更します。

**無効状態(disabled)**も適切に処理しています。 無効な場合は、透明度を下げてクリック不可の状態にします。

デフォルト値の設定も行っています。 variant || 'primary'のように、値が指定されない場合のフォールバックを用意します。

// 使用例
const ButtonShowcase = () => {
  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
      <h2>動的ボタンコンポーネント</h2>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>バリアント例</h3>
        <DynamicButton variant="primary" onClick={() => alert('Primary')}>
          Primary
        </DynamicButton>
        <DynamicButton variant="secondary" onClick={() => alert('Secondary')}>
          Secondary
        </DynamicButton>
        <DynamicButton variant="success" onClick={() => alert('Success')}>
          Success
        </DynamicButton>
        <DynamicButton variant="danger" onClick={() => alert('Danger')}>
          Danger
        </DynamicButton>
        <DynamicButton variant="warning" onClick={() => alert('Warning')}>
          Warning
        </DynamicButton>
      </div>

      <div style={{ marginBottom: '20px' }}>
        <h3>サイズ例</h3>
        <DynamicButton size="small" variant="info">Small</DynamicButton>
        <DynamicButton size="medium" variant="info">Medium</DynamicButton>
        <DynamicButton size="large" variant="info">Large</DynamicButton>
      </div>

      <div>
        <h3>状態例</h3>
        <DynamicButton variant="primary">通常</DynamicButton>
        <DynamicButton variant="primary" disabled>無効</DynamicButton>
      </div>
    </div>
  );
};

DynamicButtonコンポーネントの使用例です。

同じコンポーネントを使って、様々な見た目のボタンを作成できます。 propsを変更するだけで、色、サイズ、状態を自由に変更できます。

これがインラインスタイルの大きな利点です。 JavaScriptの変数や条件分岐を直接使えるので、動的な表現が簡単になります。

インラインスタイルの制限と対策

インラインスタイルには、いくつかの制限があります。

疑似クラス(:hover, :focus等)が使えない問題があります。 CSSでは.button:hover { background-color: red; }と書けますが、インラインでは直接指定できません。

対策として、onMouseEnter/onMouseLeave イベントで代用します。 onMouseEnter={(e) => e.target.style.backgroundColor = 'red'}のように書きます。

メディアクエリが使えない問題もあります。 CSSの@media (max-width: 768px) { ... }のような指定ができません。

対策として、window.matchMedia やライブラリを使用します。

// JS での対応例
const isMobile = window.innerWidth <= 768;
const style = {
  fontSize: isMobile ? '14px' : '16px'
};

CSS アニメーション(@keyframes)が使えない制限もあります。 複雑なアニメーションには、CSS-in-JS ライブラリまたは外部CSS併用が必要です。

// 制限への対策例
const ResponsiveInlineComponent = () => {
  const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);

  useEffect(() => {
    const handleResize = () => {
      setIsMobile(window.innerWidth <= 768);
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  const responsiveStyle = {
    padding: isMobile ? '10px' : '20px',
    fontSize: isMobile ? '14px' : '16px',
    maxWidth: isMobile ? '100%' : '600px',
    margin: '0 auto'
  };

  return (
    <div style={responsiveStyle}>
      <h2>レスポンシブ対応(JS)</h2>
      <p>画面サイズ: {isMobile ? 'モバイル' : 'デスクトップ'}</p>
    </div>
  );
};

レスポンシブ対応をJavaScriptで実装した例です。

window.innerWidthで画面幅を取得し、breakpointを判定します。 resizeイベントで画面サイズの変更を監視し、stateを更新します。

このように、JavaScriptの力を使えば、インラインスタイルでも高度な表現が可能になります。

方法3: CSS Modules

CSS Modulesの設定と基本使用

CSS Modulesは、CSSクラス名をローカルスコープ化する技術です。

CSS Modulesファイルの作成

/* src/components/ProductCard.module.css */
.card {
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  padding: 20px;
  margin: 15px 0;
  background-color: #ffffff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
  border-color: #007bff;
}

.imageContainer {
  position: relative;
  width: 100%;
  height: 200px;
  margin-bottom: 15px;
  border-radius: 8px;
  overflow: hidden;
  background-color: #f8f9fa;
}

.productImage {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.3s ease;
}

.card:hover .productImage {
  transform: scale(1.05);
}

.badge {
  position: absolute;
  top: 10px;
  right: 10px;
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.badgeNew {
  background-color: #28a745;
  color: white;
}

.badgeSale {
  background-color: #dc3545;
  color: white;
}

.badgeFeatured {
  background-color: #ffc107;
  color: #212529;
}

.content {
  padding: 0;
}

.title {
  margin: 0 0 8px 0;
  font-size: 1.25rem;
  font-weight: 600;
  color: #333;
  line-height: 1.4;
}

.description {
  margin: 0 0 15px 0;
  color: #666;
  font-size: 0.9rem;
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.priceContainer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 15px;
}

.price {
  font-size: 1.5rem;
  font-weight: 700;
  color: #28a745;
}

.originalPrice {
  font-size: 1rem;
  color: #999;
  text-decoration: line-through;
  margin-left: 8px;
}

.rating {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.9rem;
  color: #666;
}

.stars {
  color: #ffc107;
}

.actionButtons {
  display: flex;
  gap: 10px;
  margin-top: 15px;
}

.button {
  flex: 1;
  padding: 10px 16px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s ease;
  outline: none;
}

.buttonPrimary {
  background-color: #007bff;
  color: white;
}

.buttonPrimary:hover {
  background-color: #0056b3;
  transform: translateY(-1px);
}

.buttonSecondary {
  background-color: transparent;
  color: #007bff;
  border: 1px solid #007bff;
}

.buttonSecondary:hover {
  background-color: #007bff;
  color: white;
}

.buttonDisabled {
  background-color: #e9ecef;
  color: #6c757d;
  cursor: not-allowed;
}

/* レスポンシブ対応 */
@media (max-width: 768px) {
  .card {
    margin: 10px 0;
    padding: 15px;
  }
  
  .imageContainer {
    height: 150px;
  }
  
  .title {
    font-size: 1.1rem;
  }
  
  .actionButtons {
    flex-direction: column;
  }
}

プロダクトカードコンポーネントのCSS Modulesファイルです。

ファイル名は*.module.cssの形式にします。 これにより、CSSクラス名が自動的にローカルスコープ化されます。

詳細なスタイル定義を行っています。 カード、画像、バッジ、価格、ボタンなど、各要素に細かくスタイルを設定しています。

ホバー効果も豊富に実装しています。 カードのホバーで画像がズームしたり、ボタンが浮き上がったりします。

レスポンシブ対応も忘れずに実装しています。 モバイルサイズでは、ボタンを縦並びに変更しています。

CSS Modulesを使ったコンポーネント

// src/components/ProductCard.js
import React, { useState } from 'react';
import styles from './ProductCard.module.css';

const ProductCard = ({ product }) => {
  const [isLoading, setIsLoading] = useState(false);
  const { 
    id, 
    title, 
    description, 
    price, 
    originalPrice, 
    image, 
    badge, 
    rating, 
    reviewCount,
    inStock 
  } = product;

  const handleAddToCart = async () => {
    setIsLoading(true);
    // API呼び出しのシミュレーション
    await new Promise(resolve => setTimeout(resolve, 1000));
    setIsLoading(false);
    alert(`${title} をカートに追加しました!`);
  };

  const handleWishlist = () => {
    alert(`${title} をお気に入りに追加しました!`);
  };

  // バッジのスタイルを動的に決定
  const getBadgeClass = (badgeType) => {
    switch (badgeType) {
      case 'new': return `${styles.badge} ${styles.badgeNew}`;
      case 'sale': return `${styles.badge} ${styles.badgeSale}`;
      case 'featured': return `${styles.badge} ${styles.badgeFeatured}`;
      default: return styles.badge;
    }
  };

  // 星評価の生成
  const renderStars = (rating) => {
    const stars = [];
    const fullStars = Math.floor(rating);
    const hasHalfStar = rating % 1 !== 0;

    for (let i = 0; i < fullStars; i++) {
      stars.push('★');
    }
    if (hasHalfStar) {
      stars.push('☆');
    }
    while (stars.length < 5) {
      stars.push('☆');
    }
    return stars.join('');
  };

  return (
    <div className={styles.card}>
      <div className={styles.imageContainer}>
        <img 
          src={image || 'https://via.placeholder.com/300x200'} 
          alt={title}
          className={styles.productImage}
        />
        {badge && (
          <span className={getBadgeClass(badge)}>
            {badge === 'new' ? 'NEW' : badge === 'sale' ? 'SALE' : 'おすすめ'}
          </span>
        )}
      </div>

      <div className={styles.content}>
        <h3 className={styles.title}>{title}</h3>
        <p className={styles.description}>{description}</p>

        <div className={styles.priceContainer}>
          <div>
            <span className={styles.price}>¥{price?.toLocaleString()}</span>
            {originalPrice && originalPrice > price && (
              <span className={styles.originalPrice}>
                ¥{originalPrice.toLocaleString()}
              </span>
            )}
          </div>
        </div>

        {rating && (
          <div className={styles.rating}>
            <span className={styles.stars}>{renderStars(rating)}</span>
            <span>{rating}</span>
            <span>({reviewCount} レビュー)</span>
          </div>
        )}

        <div className={styles.actionButtons}>
          <button
            className={`${styles.button} ${
              !inStock ? styles.buttonDisabled : styles.buttonPrimary
            }`}
            onClick={handleAddToCart}
            disabled={!inStock || isLoading}
          >
            {isLoading ? '追加中...' : !inStock ? '在庫切れ' : 'カートに追加'}
          </button>
          
          <button
            className={`${styles.button} ${styles.buttonSecondary}`}
            onClick={handleWishlist}
          >
            ♡ お気に入り
          </button>
        </div>
      </div>
    </div>
  );
};

export default ProductCard;

CSS Modulesを使ったコンポーネントの実装です。

stylesオブジェクトのインポートから始まります。 import styles from './ProductCard.module.css';で、CSSファイルをオブジェクトとして読み込みます。

クラス名の使用は、styles.cardのようにオブジェクトのプロパティとしてアクセスします。 この時点で、クラス名は自動的にユニークな名前に変換されています。

複数クラスの組み合わせも簡単にできます。 `${styles.badge} ${styles.badgeNew}`のように、テンプレートリテラルで結合します。

条件付きクラス適用も自然に書けます。 !inStock ? styles.buttonDisabled : styles.buttonPrimaryのように、状態に応じてクラスを切り替えます。

ヘルパー関数の実装も行っています。 getBadgeClassrenderStarsのように、スタイル関連のロジックを関数化しています。

// 使用例
const ProductList = () => {
  const products = [
    {
      id: 1,
      title: 'ワイヤレスイヤホン',
      description: '高音質Bluetooth5.0対応のワイヤレスイヤホン。長時間バッテリーで快適な音楽体験を提供します。',
      price: 8900,
      originalPrice: 12000,
      image: 'https://via.placeholder.com/300x200/007bff/fff',
      badge: 'sale',
      rating: 4.5,
      reviewCount: 128,
      inStock: true
    },
    {
      id: 2,
      title: 'スマートウォッチ',
      description: '健康管理機能搭載のスマートウォッチ。心拍数、歩数、睡眠の質を24時間モニタリング。',
      price: 15800,
      image: 'https://via.placeholder.com/300x200/28a745/fff',
      badge: 'new',
      rating: 4.2,
      reviewCount: 89,
      inStock: true
    },
    {
      id: 3,
      title: 'ノートパソコンスタンド',
      description: '調整可能なアルミニウム製ノートパソコンスタンド。エルゴノミクスデザインで作業効率アップ。',
      price: 3200,
      image: 'https://via.placeholder.com/300x200/ffc107/000',
      badge: 'featured',
      rating: 4.8,
      reviewCount: 256,
      inStock: false
    }
  ];

  return (
    <div style={{ 
      display: 'grid', 
      gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', 
      gap: '20px',
      padding: '20px',
      maxWidth: '1200px',
      margin: '0 auto'
    }}>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
};

ProductCardコンポーネントの使用例です。

Grid レイアウトを使って、カードを自動的に配置しています。 repeat(auto-fit, minmax(300px, 1fr))で、画面幅に応じて列数が調整されます。

各商品データには、様々な情報を設定しています。 価格、画像、バッジ、評価、在庫状況など、実際のECサイトのような情報です。

CSS Modulesの高度な機能

CSS Modulesでは、より高度な機能も使用できます。

/* src/components/Navigation.module.css */

/* CSS変数の活用 */
.navigation {
  --primary-color: #007bff;
  --secondary-color: #6c757d;
  --text-color: #333;
  --border-color: #e9ecef;
  --shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  
  background-color: white;
  border-bottom: 1px solid var(--border-color);
  box-shadow: var(--shadow);
  position: sticky;
  top: 0;
  z-index: 100;
}

/* 複合セレクタ */
.navigation .container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 60px;
}

.logo {
  font-size: 1.5rem;
  font-weight: bold;
  color: var(--primary-color);
  text-decoration: none;
  transition: color 0.3s ease;
}

.logo:hover {
  color: var(--secondary-color);
}

.navList {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
  gap: 30px;
}

.navItem {
  position: relative;
}

.navLink {
  color: var(--text-color);
  text-decoration: none;
  font-weight: 500;
  padding: 8px 0;
  transition: color 0.3s ease;
  position: relative;
}

.navLink:hover {
  color: var(--primary-color);
}

/* 疑似要素を使ったアンダーライン */
.navLink::after {
  content: '';
  position: absolute;
  bottom: -5px;
  left: 0;
  width: 0;
  height: 2px;
  background-color: var(--primary-color);
  transition: width 0.3s ease;
}

.navLink:hover::after,
.navLink:global(.active)::after {
  width: 100%;
}

/* モバイルメニュー */
.mobileMenuButton {
  display: none;
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: var(--text-color);
}

@media (max-width: 768px) {
  .navList {
    position: fixed;
    top: 60px;
    left: 0;
    right: 0;
    background-color: white;
    flex-direction: column;
    padding: 20px;
    box-shadow: var(--shadow);
    transform: translateY(-100%);
    opacity: 0;
    visibility: hidden;
    transition: all 0.3s ease;
  }
  
  .navList:global(.open) {
    transform: translateY(0);
    opacity: 1;
    visibility: visible;
  }
  
  .mobileMenuButton {
    display: block;
  }
  
  .navItem {
    margin: 10px 0;
  }
}

/* ダークテーマ対応 */
:global(.dark-theme) .navigation {
  --text-color: #f8f9fa;
  --border-color: #495057;
  
  background-color: #343a40;
  border-bottom-color: var(--border-color);
}

/* アニメーション */
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.fadeInUp {
  animation: fadeInUp 0.5s ease-out;
}

**CSS変数(カスタムプロパティ)**を活用しています。 --primary-colorのように定義して、var(--primary-color)で使用します。 テーマ変更や一括修正が簡単になります。

:global()セレクタで、グローバルスコープのクラスと連携できます。 .navLink:global(.active)のように書くことで、外部から付与されるクラスにも対応できます。

疑似要素や**@keyframes**も通常のCSSと同様に使用できます。 CSS Modulesの利点を保ちながら、CSSの全機能を活用できます。

// src/components/Navigation.js
import React, { useState } from 'react';
import styles from './Navigation.module.css';

const Navigation = () => {
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
  const [activeLink, setActiveLink] = useState('home');

  const navItems = [
    { id: 'home', label: 'ホーム', href: '#home' },
    { id: 'about', label: '私について', href: '#about' },
    { id: 'services', label: 'サービス', href: '#services' },
    { id: 'portfolio', label: 'ポートフォリオ', href: '#portfolio' },
    { id: 'contact', label: 'お問い合わせ', href: '#contact' }
  ];

  const handleLinkClick = (id) => {
    setActiveLink(id);
    setIsMobileMenuOpen(false);
  };

  const toggleMobileMenu = () => {
    setIsMobileMenuOpen(!isMobileMenuOpen);
  };

  return (
    <nav className={styles.navigation}>
      <div className={styles.container}>
        <a href="#home" className={styles.logo}>
          MyPortfolio
        </a>

        <ul className={`${styles.navList} ${isMobileMenuOpen ? 'open' : ''}`}>
          {navItems.map(item => (
            <li key={item.id} className={styles.navItem}>
              <a
                href={item.href}
                className={`${styles.navLink} ${activeLink === item.id ? 'active' : ''}`}
                onClick={() => handleLinkClick(item.id)}
              >
                {item.label}
              </a>
            </li>
          ))}
        </ul>

        <button
          className={styles.mobileMenuButton}
          onClick={toggleMobileMenu}
          aria-label="メニューを開く"
        >
          {isMobileMenuOpen ? '✕' : '☰'}
        </button>
      </div>
    </nav>
  );
};

export default Navigation;

ナビゲーションコンポーネントの実装です。

グローバルクラスとの組み合わせを行っています。 'open''active'のようなグローバルクラスを、ローカルクラスと併用しています。

モバイル対応も実装しています。 画面サイズに応じて、ハンバーガーメニューを表示・非表示切り替えます。

アクセシビリティにも配慮しています。 aria-label属性で、スクリーンリーダー用の説明を提供しています。

3つの方法の使い分け

具体的な使い分けガイド

プロジェクトや用途に応じた最適な選択方法を見てみましょう。

用途別の推奨方法

静的なコンポーネント(ヘッダー、フッター等)では、外部CSS または CSS Modulesがおすすめです。 複雑なスタイルやメディアクエリが必要だからです。

動的なスタイルが必要なコンポーネントでは、インライン + 外部CSSの組み合わせが効果的です。 状態に応じたスタイル変更が簡単だからです。

再利用可能なUIコンポーネントでは、CSS Modulesが最適です。 スコープ管理と保守性のバランスが良いからです。

プロトタイプや小規模プロジェクトでは、インライン + 外部CSSがおすすめです。 開発速度重視で、設定不要だからです。

本格的なプロダクション環境では、CSS Modules + CSS-in-JSの組み合わせが推奨されます。 保守性、パフォーマンス、チーム開発に優れているからです。

// 実際の組み合わせ例
const HybridStylingExample = () => {
  const [theme, setTheme] = useState('light');
  const [isLoading, setIsLoading] = useState(false);

  // インラインで動的スタイル
  const containerStyle = {
    backgroundColor: theme === 'light' ? '#ffffff' : '#333333',
    color: theme === 'light' ? '#333333' : '#ffffff',
    transition: 'all 0.3s ease',
    minHeight: '100vh'
  };

  const loadingOverlayStyle = {
    position: 'fixed',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    display: isLoading ? 'flex' : 'none',
    alignItems: 'center',
    justifyContent: 'center',
    zIndex: 9999
  };

  return (
    <div style={containerStyle}>
      {/* CSS Modulesで静的スタイル */}
      <Navigation />
      
      {/* 外部CSSで基本レイアウト */}
      <main className="main-content">
        <section className="hero-section">
          <h1>ハイブリッドスタイリング</h1>
          
          {/* インラインで動的スタイル */}
          <button
            style={{
              backgroundColor: theme === 'light' ? '#007bff' : '#ffc107',
              color: theme === 'light' ? 'white' : 'black',
              border: 'none',
              padding: '12px 24px',
              borderRadius: '6px',
              cursor: 'pointer',
              fontSize: '16px',
              transition: 'all 0.3s ease'
            }}
            onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
          >
            テーマ切替
          </button>
        </section>
      </main>

      {/* インラインで条件付きスタイル */}
      <div style={loadingOverlayStyle}>
        <div style={{
          backgroundColor: 'white',
          padding: '20px',
          borderRadius: '8px',
          textAlign: 'center'
        }}>
          <p>読み込み中...</p>
        </div>
      </div>
    </div>
  );
};

ハイブリッドなアプローチの実装例です。

CSS Modulesで静的なナビゲーション、外部CSSで基本レイアウト、インラインで動的スタイルを使い分けています。 それぞれの得意分野を活かした組み合わせになっています。

テーマ切り替えのような動的な部分はインラインで、ローディングオーバーレイのような条件付き表示もインラインで実装しています。

パフォーマンス比較

各手法には、それぞれパフォーマンス特性があります。

外部CSSは、未使用スタイルも含むため中程度のバンドルサイズになります。 しかし、CSSエンジンの最適化により高いランタイムパフォーマンスを発揮します。 ブラウザキャッシュも効果的で、初回以降の読み込みが高速になります。

インラインスタイルは、使用分のみなので小さなバンドルサイズになります。 style属性の処理により、ランタイムパフォーマンスは中程度です。 HTMLに含まれるため、キャッシュ効果は期待できません。

CSS Modulesは、使用分のみ+コンパイル処理で小〜中程度のバンドルサイズになります。 通常のCSSとして処理されるため、高いランタイムパフォーマンスを保ちます。 ハッシュ化されたクラス名により、良好なキャッシュ効果があります。

実践的な組み合わせパターン

実際のプロジェクトでの推奨される組み合わせパターンをご紹介します。

スモールプロジェクトでは、以下の構成がおすすめです。

  • グローバル: 外部CSS(リセットCSS、共通スタイル)
  • コンポーネント: インライン(動的スタイル)
  • レイアウト: 外部CSS(レスポンシブ、グリッド)

具体的には、src/index.cssでグローバルスタイル、各コンポーネントでstyle属性、src/layout.cssでレイアウトという構成です。

ミディアムプロジェクトでは、以下の構成が適しています。

  • グローバル: 外部CSS(ベーススタイル)
  • コンポーネント: CSS Modules(静的スタイル)
  • 動的: インライン(動的スタイル)

src/styles/global.css、src/components/Header/Header.module.css、動的部分はstyle属性という構成です。

ラージプロジェクトでは、以下の構成を推奨します。

  • デザインシステム: CSS Modules(デザインシステム)
  • コンポーネント: CSS Modules(コンポーネント固有)
  • テーマ: CSS変数 + Context API
  • 動的: CSS-in-JS ライブラリ

src/styles/design-system.module.css、src/components/**/*.module.css、src/context/ThemeContext.js、styled-componentsなどの構成です。

まとめ

Reactでのスタイリングには、それぞれ異なる特徴と適用場面がある3つの主要な方法があります。

この記事で学んだポイントを整理すると以下のようになります。

3つのスタイリング方法

1. 外部CSSファイル

慣れ親しんだCSS記法で書けます。 疑似クラスやメディアクエリにも対応しています。 ただし、グローバルスコープによる影響範囲に注意が必要です。 大規模プロジェクトに適しています。

2. インラインスタイル

コンポーネントスコープで安全に使えます。 動的スタイル変更が簡単にできます。 ただし、CSS機能に制限があります(疑似クラス、メディアクエリ不可)。 プロトタイプや小規模プロジェクトに適しています。

3. CSS Modules

ローカルスコープによるクラス名競合回避ができます。 CSS全機能の利用が可能です。 ただし、設定が必要で学習コストがかかります。 中〜大規模プロジェクトに適しています。

選択の指針

プロジェクト規模に応じて選択しましょう。 小規模はインライン、大規模はCSS Modulesがおすすめです。

チーム開発では、CSS ModulesでスコープとルールSpiel統一が重要です。

動的スタイルには、インラインとイベントハンドラーの組み合わせが効果的です。

保守性重視なら、CSS Modulesで明確な責任分離を行いましょう。

実践的なアプローチ

単一手法に固執せず、適材適所で組み合わせることが大切です。 プロジェクトの成長に合わせて、スタイリング戦略を発展させましょう。 チーム内でのコーディング規約とベストプラクティスの共有も重要です。

パフォーマンス考慮事項

バンドルサイズと実行時パフォーマンスのトレードオフを理解しましょう。 キャッシュ効率とメンテナンス性のバランスを考慮することが重要です。 プロジェクトフェーズに応じた最適化戦略を立てましょう。

Reactでのスタイリングは、プロジェクトの要件と特性を理解した上で適切な手法を選択することが重要です。

各手法の特徴を理解し、実際のプロジェクトで試しながら最適なアプローチを見つけてください。

何より大切なのは、一貫性のあるスタイリング戦略を維持し、チーム全体で共通の理解を持つことです。

ぜひこの記事を参考にして、効果的なReactスタイリングを実践してください!

関連記事