デザインについての学習メモブログ

React入門 #19 – スタイリング手法の選び方 :5つの主要なスタイリング

記事内に広告が含まれています。

React入門 #19 – スタイリング手法の選び方 :5つの主要なスタイリング

Reactアプリケーションのスタイリングには、様々な方法があります。

この記事では、主要なスタイリング手法を比較し、それぞれの使い方、メリット・デメリット、使い分けのポイントを詳しく解説します。

インラインスタイル

style属性を使ってJSX要素に直接スタイルを当てる方法と、propsに応じてスタイルをオブジェクトとして切り替えるテクニックが身につきます。

あわせて、インラインスタイルが「使えない」場面(hover、メディアクエリなど)を理解し、他の手法を選ぶべきタイミングの判断材料が得られます。

基本的な使い方

React.CSSProperties型を使ったスタイルオブジェクトの定義方法と、JSX内に直接スタイルを書き込む2つの書き方(変数に切り出す方法とインラインで直接記述する方法)を紹介します。

TSX
function Button() {
  const buttonStyle: React.CSSProperties = {
    backgroundColor: '#3b82f6',
    color: 'white',
    padding: '10px 20px',
    border: 'none',
    borderRadius: '8px',
    cursor: 'pointer'
  }
  
  return <button style={buttonStyle}>クリック</button>
}

export default Button

または直接記述

TSX

 function Button() {
   return (
     <button style={{ 
       backgroundColor: '#3b82f6', 
       color: 'white',
       padding: '10px 20px' 
     }}>
       クリック
     </button>
   )
 }
 
export default Button

動的スタイル

variantsizeといったpropsの値に応じて、複数のスタイルオブジェクトをスプレッド構文で合成し、動的に切り替える実装パターンを解説します。

TSX
interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger'
  size: 'small' | 'medium' | 'large'
}

function Button({ variant, size }: ButtonProps) {
  const variantStyles = {
    primary: { backgroundColor: '#3b82f6' },
    secondary: { backgroundColor: '#6b7280' },
    danger: { backgroundColor: '#ef4444' }
  }
  
  const sizeStyles = {
    small: { padding: '6px 12px', fontSize: '14px' },
    medium: { padding: '10px 20px', fontSize: '16px' },
    large: { padding: '14px 28px', fontSize: '18px' }
  }
  
  const buttonStyle: React.CSSProperties = {
    ...variantStyles[variant],
    ...sizeStyles[size],
    color: 'white',
    border: 'none',
    borderRadius: '8px',
    cursor: 'pointer'
  }
  
  return <button style={buttonStyle}>クリック</button>
}

export default Button

メリット:

  • JavaScriptで動的に変更できる
  • スコープが明確
  • 追加の設定不要

デメリット:

  • 疑似クラス(:hover、:focus)が使えない
  • メディアクエリが使えない
  • パフォーマンスが劣る
  • 再利用性が低い

通常のCSS

.cssファイルを別途用意してクラス名でスタイルを当てる、最も基礎的なスタイリング方法が身につきます。CSSのすべての機能(hover、メディアクエリなど)が使える反面、クラス名がグローバルスコープになるという根本的な制約も理解できます。

基本的な使い方

App.tsxからApp.cssをインポートし、className属性でクラスを指定する、従来からあるCSSの使い方を示します。

TSX
// App.tsx
import './App.css'

function App() {
  return (
    <div className="container">
      <h1 className="title">タイトル</h1>
      <button className="btn btn-primary">クリック</button>
    </div>
  )
}
CSS
/* App.css */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.title {
  font-size: 2rem;
  color: #1f2937;
  margin-bottom: 20px;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 16px;
}

.btn-primary {
  background-color: #3b82f6;
  color: white;
}

.btn-primary:hover {
  background-color: #2563eb;
}

メリット:

  • シンプルで理解しやすい
  • すべてのCSS機能が使える
  • ファイルサイズが小さい

デメリット:

  • グローバルスコープ(名前の衝突)
  • 未使用CSSの管理が難しい
  • コンポーネントとの結びつきが弱い

CSS Modules

ファイル単位でクラス名を自動的にローカルスコープ化するCSS Modulesの仕組みと、classnamesライブラリを組み合わせて条件付きクラスを管理する実践的な書き方が身につきます。

記事内の「完全なカードコンポーネント」の例を通じて、実務レベルのコンポーネント設計にも触れられます。

基本的な使い方

.module.cssという拡張子で作成したCSSファイルが、importするとstyles.buttonのようなオブジェクトとして扱える仕組みと、その基本的な利用方法を説明します。

CSS
// Button.module.css
.button {
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 16px;
}

.primary {
  background-color: #3b82f6;
  color: white;
}

.primary:hover {
  background-color: #2563eb;
}

.secondary {
  background-color: #6b7280;
  color: white;
}
TSX
// Button.tsx
import styles from './Button.module.css'

interface ButtonProps {
  variant?: 'primary' | 'secondary'
  children: React.ReactNode
}

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

classnames ライブラリの使用

複数のクラス名や条件付きクラスを文字列結合で書くと読みにくくなる問題を、classnamesライブラリでどのように解決するかを示します。

Bash
npm install classnames
TSX
import styles from './Button.module.css'
import classNames from 'classnames'

function Button({ variant = 'primary', disabled, children }: ButtonProps) {
  return (
    <button 
      className={classNames(
        styles.button,
        styles[variant],
        { [styles.disabled]: disabled }
      )}
      disabled={disabled}
    >
      {children}
    </button>
  )
}

コンポーネントの完全な例

バッジ付きのCardコンポーネントを例に、CSS Modulesとclassnamesを組み合わせた、より実践的で複雑なコンポーネントの実装方法を紹介します。

CSS
// Card.module.css
.card {
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  transition: transform 0.2s, box-shadow 0.2s;
}

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

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

.title {
  font-size: 1.5rem;
  font-weight: 600;
  color: #1f2937;
  margin: 0;
}

.badge {
  padding: 4px 12px;
  border-radius: 16px;
  font-size: 0.875rem;
  font-weight: 500;
}

.badgeSuccess {
  background-color: #d1fae5;
  color: #065f46;
}

.badgeWarning {
  background-color: #fef3c7;
  color: #92400e;
}

.content {
  color: #6b7280;
  line-height: 1.6;
}

.footer {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid #e5e7eb;
}
TSX
// Card.tsx
import styles from './Card.module.css'
import classNames from 'classnames'

interface CardProps {
  title: string
  badge?: {
    text: string
    variant: 'success' | 'warning'
  }
  children: React.ReactNode
  footer?: React.ReactNode
}

function Card({ title, badge, children, footer }: CardProps) {
  return (
    <div className={styles.card}>
      <div className={styles.header}>
        <h2 className={styles.title}>{title}</h2>
        {badge && (
          <span className={classNames(
            styles.badge,
            badge.variant === 'success' ? styles.badgeSuccess : styles.badgeWarning
          )}>
            {badge.text}
          </span>
        )}
      </div>
      <div className={styles.content}>
        {children}
      </div>
      {footer && (
        <div className={styles.footer}>
          {footer}
        </div>
      )}
    </div>
  )
}

メリット:

  • ローカルスコープ(名前の衝突を防ぐ)
  • すべてのCSS機能が使える
  • コンポーネントと一緒に管理できる
  • TypeScriptの補完が効く

デメリット:

  • クラス名が長くなる
  • 動的なスタイルが少し複雑

Tailwind CSS

ユーティリティクラスを直接JSXに書き込む現代的なスタイリング手法を、セットアップからカスタマイズまで一通り身につけられます。

条件付きクラスの管理、独自カラーパレットの追加、@applyによるクラスの再利用といった、実務で頻出するテクニックも合わせて理解できます。

セットアップ

Tailwind CSSをプロジェクトに導入するためのパッケージインストールと、設定ファイル・CSSファイルへの基本的な記述方法を説明します。

Bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
JavaScript
// tailwind.config.js
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
CSS
/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

基本的な使い方

bg-blue-500rounded-lgといったユーティリティクラスをclassNameに並べるだけでスタイリングが完成する、Tailwindの基本的な書き方をButtonとCardの例で示します。

TSX
function Button({ children }: { children: React.ReactNode }) {
  return (
    <button className="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg transition-colors">
      {children}
    </button>
  )
}

function Card() {
  return (
    <div className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
      <h2 className="text-2xl font-bold text-gray-800 mb-4">
        カードタイトル
      </h2>
      <p className="text-gray-600 leading-relaxed">
        カードの内容がここに入ります
      </p>
    </div>
  )
}

条件付きクラス

propsの値(variant、size、disabledなど)によってTailwindのクラスを切り替えたい場合に、classnamesライブラリのオブジェクト記法を使って可読性を保つ方法を解説します。

TSX
import classNames from 'classnames'

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  children: React.ReactNode
}

function Button({ 
  variant = 'primary', 
  size = 'md', 
  disabled = false,
  children 
}: ButtonProps) {
  return (
    <button
      className={classNames(
        // 基本スタイル
        'font-semibold rounded-lg transition-colors',
        // バリアント
        {
          'bg-blue-500 hover:bg-blue-600 text-white': variant === 'primary',
          'bg-gray-500 hover:bg-gray-600 text-white': variant === 'secondary',
          'bg-red-500 hover:bg-red-600 text-white': variant === 'danger',
        },
        // サイズ
        {
          'py-1 px-3 text-sm': size === 'sm',
          'py-2 px-4 text-base': size === 'md',
          'py-3 px-6 text-lg': size === 'lg',
        },
        // 無効状態
        {
          'opacity-50 cursor-not-allowed': disabled,
        }
      )}
      disabled={disabled}
    >
      {children}
    </button>
  )
}

カスタムコンポーネント

レスポンシブなグリッドレイアウトと、画像のホバーエフェクトやバッジ表示を含む実践的なProductCardコンポーネントを例に、Tailwindでより複雑なUIを組み立てる流れを示します。

TSX
// レスポンシブなグリッドレイアウト
function ProductGrid({ products }: { products: Product[] }) {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

// 複雑なカードコンポーネント
function ProductCard({ product }: { product: Product }) {
  return (
    <div className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 group">
      <div className="relative h-48 overflow-hidden">
        <img 
          src={product.image} 
          alt={product.name}
          className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
        />
        {product.isNew && (
          <span className="absolute top-2 right-2 bg-red-500 text-white text-xs font-bold px-2 py-1 rounded">
            NEW
          </span>
        )}
      </div>
      
      <div className="p-4">
        <h3 className="text-lg font-semibold text-gray-800 mb-2 truncate">
          {product.name}
        </h3>
        <p className="text-gray-600 text-sm mb-4 line-clamp-2">
          {product.description}
        </p>
        
        <div className="flex items-center justify-between">
          <span className="text-2xl font-bold text-blue-600">
            ¥{product.price.toLocaleString()}
          </span>
          <button className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">
            カートに追加
          </button>
        </div>
      </div>
    </div>
  )
}

カスタムユーティリティの追加

デフォルトのカラーパレットでは足りない場合に、tailwind.config.jstheme.extendでブランドカラーや独自の余白サイズを追加する拡張方法を説明します。

JavaScript
// tailwind.config.js
export default {
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eff6ff',
          100: '#dbeafe',
          500: '#3b82f6',
          600: '#2563eb',
          900: '#1e3a8a',
        }
      },
      spacing: {
        '128': '32rem',
      }
    },
  },
}
TSX
// カスタムカラーの使用
<button className="bg-brand-500 hover:bg-brand-600 text-white">
  クリック
</button>

@apply ディレクティブ

同じユーティリティクラスの組み合わせを何度も書く代わりに、@applyを使って独自のCSSクラス(.btn-primaryなど)にまとめる、コードの重複を減らすテクニックを紹介します。

CSS
/* styles.css */
@layer components {
  .btn {
    @apply font-semibold py-2 px-4 rounded-lg transition-colors;
  }
  
  .btn-primary {
    @apply bg-blue-500 hover:bg-blue-600 text-white;
  }
  
  .btn-secondary {
    @apply bg-gray-500 hover:bg-gray-600 text-white;
  }
  
  .card {
    @apply bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow;
  }
}
TSX
// コンポーネントで使用
function Button({ variant = 'primary', children }: ButtonProps) {
  return (
    <button className={`btn btn-${variant}`}>
      {children}
    </button>
  )
}

メリット:

  • 高速な開発
  • 一貫性のあるデザイン
  • レスポンシブ対応が簡単
  • ファイル切り替えが不要
  • 本番環境で未使用CSSが削除される

デメリット:

  • クラス名が長くなる
  • 学習曲線がある
  • HTMLが複雑に見える

CSS-in-JS(Styled-components)

JavaScriptファイルの中にCSSを直接書き込むStyled-componentsの基本構文と、ThemeProviderを使ってアプリ全体でカラーやスペーシングなどのデザイントークンを一元管理する方法が身につきます。

セットアップ

Styled-componentsとTypeScript用の型定義パッケージをインストールする手順を示します。

Bash
npm install styled-components
npm install -D @types/styled-components

基本的な使い方

テンプレートリテラルの中にCSSをそのまま書き、propsを使ってhover時の色やバリアントごとの背景色を動的に切り替える、Styled-componentsの中核となる書き方を解説します。

TSX
import styled from 'styled-components'

const Button = styled.button<{ variant?: 'primary' | 'secondary' }>`
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.2s;
  
  background-color: ${props => 
    props.variant === 'secondary' ? '#6b7280' : '#3b82f6'
  };
  color: white;
  
  &:hover {
    background-color: ${props => 
      props.variant === 'secondary' ? '#4b5563' : '#2563eb'
    };
  }
  
  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`

const Card = styled.div`
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  transition: all 0.2s;
  
  &:hover {
    transform: translateY(-4px);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  }
`

const CardTitle = styled.h2`
  font-size: 1.5rem;
  font-weight: 600;
  color: #1f2937;
  margin: 0 0 16px 0;
`

function ProductCard({ title, description }: { title: string, description: string }) {
  return (
    <Card>
      <CardTitle>{title}</CardTitle>
      <p>{description}</p>
      <Button variant="primary">購入する</Button>
    </Card>
  )
}

テーマの使用

カラーやスペーシング、角丸の値などをthemeオブジェクトとして定義し、ThemeProviderでアプリ全体に配布することで、デザインの一貫性を保ちながら一括変更できるようにする方法を説明します。

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

const theme = {
  colors: {
    primary: '#3b82f6',
    secondary: '#6b7280',
    danger: '#ef4444',
    text: '#1f2937',
  },
  spacing: {
    sm: '8px',
    md: '16px',
    lg: '24px',
  },
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '12px',
  }
}

const Button = styled.button`
  background-color: ${props => props.theme.colors.primary};
  padding: ${props => props.theme.spacing.md};
  border-radius: ${props => props.theme.borderRadius.md};
  color: white;
  border: none;
  cursor: pointer;
`

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Button>クリック</Button>
    </ThemeProvider>
  )
}

メリット:

  • JavaScriptの力を活用できる
  • 動的なスタイリングが簡単
  • テーマの管理が容易
  • スコープが自動的にローカル

デメリット:

  • ランタイムオーバーヘッド
  • ファイルサイズが大きい
  • デバッグが難しい

スタイリング手法の比較

これまで紹介した5つの手法を、スコープ・動的スタイル対応・パフォーマンス・学習コスト・適したチーム規模という5つの観点で一覧比較できるようになり、自分のプロジェクトに合う手法を素早く絞り込めるようになります。

手法スコープ動的スタイルパフォーマンス学習コストチーム規模
インラインローカル
通常のCSSグローバル小〜中
CSS Modulesローカル中〜大
CSS-in-JSローカル中〜大
Tailwindローカル中〜大

使い分けのガイドライン

どんな状況でどの手法を選ぶべきか」という実務上の判断基準を、プロジェクト規模・チームのスキル・動的スタイルの必要性といった観点から具体的に持てるようになります。

インラインスタイルを使う場合

  • 本当に動的なスタイル
  • 一時的なプロトタイプ

通常のCSSを使う場合

  • 小規模プロジェクト
  • シンプルなサイト
  • チームがCSSに慣れている

CSS Modulesを使う場合

  • 中〜大規模プロジェクト
  • コンポーネントごとにスタイルを分離したい
  • 既存のCSSスキルを活かしたい

Tailwind CSSを使う場合(推奨)

  • 高速な開発が必要
  • 一貫性のあるデザインシステム
  • レスポンシブ対応が多い
  • モダンなプロジェクト

CSS-in-JSを使う場合

  • 高度に動的なスタイル
  • テーマ切り替えが必要
  • JavaScriptでスタイルを管理したい

実践:完全なコンポーネント

これまで学んだTailwind CSSの知識を総動員し、優先度ごとに見た目が変わるTodoアイテムという、実務でそのまま使えるレベルのコンポーネントを組み立てる力が身につきます。

Tailwind CSSでの実装

優先度(high/medium/low)に応じた枠線色、完了状態に応じた打ち消し線や透明度の変化を、classnamesとTailwindの組み合わせで実装するTodoItemコンポーネントの完成形を示します。

TSX
interface TodoItemProps {
  todo: {
    id: number
    text: string
    completed: boolean
    priority: 'high' | 'medium' | 'low'
  }
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}

function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
  const priorityColors = {
    high: 'border-l-red-500 bg-red-50',
    medium: 'border-l-yellow-500 bg-yellow-50',
    low: 'border-l-green-500 bg-green-50'
  }
  
  return (
    <div className={classNames(
      'flex items-center gap-3 p-4 rounded-lg border-l-4 transition-all',
      priorityColors[todo.priority],
      {
        'opacity-60': todo.completed
      }
    )}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        className="w-5 h-5 rounded cursor-pointer"
      />
      
      <span className={classNames(
        'flex-1 text-lg',
        {
          'line-through text-gray-500': todo.completed,
          'text-gray-800': !todo.completed
        }
      )}>
        {todo.text}
      </span>
      
      <button
        onClick={() => onDelete(todo.id)}
        className="px-3 py-1 text-sm text-red-600 hover:bg-red-100 rounded transition-colors"
      >
        削除
      </button>
    </div>
  )
}

まとめ

この記事では、Reactの様々なスタイリング手法を詳しく学びました。

重要なポイント:

  • プロジェクトの規模とチームに応じて選択
  • CSS Modulesは学習コストが低い
  • Tailwind CSSは開発速度が速い
  • CSS-in-JSは動的スタイルに強い

推奨するスタイリング手法:

  1. 小規模プロジェクト: 通常のCSS
  2. 中規模プロジェクト: CSS Modules
  3. 大規模プロジェクト: Tailwind CSS
  4. 高度な動的スタイル: CSS-in-JS

次のステップ: 次回は、本番環境へのデプロイについて学びます。

Vercel、Netlify、AWS S3など、様々なデプロイ方法を詳しく解説します!

スタイリング手法に正解はありません。プロジェクトの要件とチームの好みに合わせて選びましょう!