T-CREATOR

Emotion の composition(合成)を使いこなすコツ

Emotion の composition(合成)を使いこなすコツ

React 開発において、スタイリングは避けて通れない重要な要素です。特に大規模なプロジェクトでは、一貫性のあるデザインを保ちながら、柔軟性も求められます。

Emotion の composition(合成)機能は、まさにこの課題を解決する魔法のような機能です。複数のスタイルを組み合わせることで、再利用可能で保守性の高いコンポーネントを作成できます。

この記事では、Emotion の composition の基本から実践的な活用方法まで、実際のコード例とエラーケースを交えて詳しく解説します。読み終えた頃には、あなたも Emotion の composition を使いこなせるようになっているはずです。

背景

CSS-in-JS と Emotion の位置づけ

CSS-in-JS は、JavaScript 内で CSS を記述する手法です。従来の CSS ファイルとは異なり、コンポーネントとスタイルを密接に結びつけることができます。

Emotion は、この CSS-in-JS ライブラリの中でも特に人気が高く、直感的な API と優れたパフォーマンスを提供します。

javascript// 従来の CSS ファイル
.button {
  background: blue;
  color: white;
  padding: 10px 20px;
}

// Emotion での記述
const Button = styled.button`
  background: blue;
  color: white;
  padding: 10px 20px;
`;

Emotion の最大の特徴は、この styled コンポーネント同士を組み合わせる composition 機能にあります。これにより、小さなスタイルの部品から複雑な UI を構築できるようになります。

課題

従来の CSS クラス管理の限界と composition が必要な理由

従来の CSS クラス管理では、以下のような課題に直面します:

クラス名の衝突

css/* 複数の開発者が同じクラス名を使用してしまう */
.button {
  background: red;
}

/* 別のファイルで同じクラス名 */
.button {
  background: blue; /* 上書きされてしまう */
}

スタイルの依存関係が不明確

css/* どのクラスがどのクラスに依存しているか分からない */
.primary-button {
  /* 基本ボタンスタイル */
}

.primary-button:hover {
  /* ホバー時のスタイル */
}

/* この依存関係が複雑になると管理が困難 */

再利用性の低さ

css/* 似たようなスタイルを何度も書く必要がある */
.small-button {
  padding: 5px 10px;
  font-size: 12px;
}

.medium-button {
  padding: 10px 20px;
  font-size: 14px;
}

.large-button {
  padding: 15px 30px;
  font-size: 16px;
}

これらの課題を解決するのが、Emotion の composition 機能です。スタイルを部品化し、組み合わせることで、保守性と再利用性を大幅に向上させることができます。

解決策

composition の基本構文

Emotion の composition は、css 関数と styled コンポーネントを組み合わせることで実現します。

javascriptimport styled, { css } from '@emotion/styled';

// 基本スタイルを定義
const baseButtonStyles = css`
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.2s ease;
`;

// サイズバリエーション
const smallButton = css`
  padding: 8px 16px;
  font-size: 14px;
`;

const largeButton = css`
  padding: 16px 32px;
  font-size: 18px;
`;

// 色バリエーション
const primaryButton = css`
  background: #007bff;
  color: white;

  &:hover {
    background: #0056b3;
  }
`;

const secondaryButton = css`
  background: #6c757d;
  color: white;

  &:hover {
    background: #545b62;
  }
`;

複数のスタイルを組み合わせる方法

定義したスタイルを組み合わせて、実際のコンポーネントを作成します。

javascript// 基本ボタンコンポーネント
const Button = styled.button`
  ${baseButtonStyles}
`;

// サイズと色を組み合わせたバリエーション
const SmallPrimaryButton = styled.button`
  ${baseButtonStyles}
  ${smallButton}
  ${primaryButton}
`;

const LargeSecondaryButton = styled.button`
  ${baseButtonStyles}
  ${largeButton}
  ${secondaryButton}
`;

条件付き composition の実装

props を使って動的にスタイルを組み合わせることも可能です。

javascript// 条件付きスタイルの定義
const getSizeStyles = (size) => {
  switch (size) {
    case 'small':
      return smallButton;
    case 'large':
      return largeButton;
    default:
      return css`
        padding: 12px 24px;
        font-size: 16px;
      `;
  }
};

const getVariantStyles = (variant) => {
  switch (variant) {
    case 'primary':
      return primaryButton;
    case 'secondary':
      return secondaryButton;
    default:
      return css`
        background: #e9ecef;
        color: #495057;

        &:hover {
          background: #dee2e6;
        }
      `;
  }
};

// 動的な composition
const DynamicButton = styled.button`
  ${baseButtonStyles}
  ${(props) => getSizeStyles(props.size)}
  ${(props) => getVariantStyles(props.variant)}
`;

具体例

ボタンコンポーネントでの活用

実際のプロジェクトでよく使われるボタンコンポーネントの例を見てみましょう。

javascript// ボタンの基本スタイル
const buttonBase = css`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-family: inherit;
  font-weight: 500;
  text-decoration: none;
  transition: all 0.2s ease;

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
`;

// サイズバリエーション
const buttonSizes = {
  xs: css`
    padding: 4px 8px;
    font-size: 12px;
    min-height: 24px;
  `,
  sm: css`
    padding: 6px 12px;
    font-size: 14px;
    min-height: 32px;
  `,
  md: css`
    padding: 8px 16px;
    font-size: 16px;
    min-height: 40px;
  `,
  lg: css`
    padding: 12px 24px;
    font-size: 18px;
    min-height: 48px;
  `,
};
javascript// 色バリエーション
const buttonVariants = {
  primary: css`
    background: #3b82f6;
    color: white;

    &:hover:not(:disabled) {
      background: #2563eb;
    }

    &:active:not(:disabled) {
      background: #1d4ed8;
    }
  `,
  secondary: css`
    background: #6b7280;
    color: white;

    &:hover:not(:disabled) {
      background: #4b5563;
    }

    &:active:not(:disabled) {
      background: #374151;
    }
  `,
  outline: css`
    background: transparent;
    color: #3b82f6;
    border: 2px solid #3b82f6;

    &:hover:not(:disabled) {
      background: #3b82f6;
      color: white;
    }
  `,
};
javascript// 完成したボタンコンポーネント
const Button = styled.button`
  ${buttonBase}
  ${(props) => buttonSizes[props.size] || buttonSizes.md}
  ${(props) =>
    buttonVariants[props.variant] || buttonVariants.primary}
`;

// 使用例
const App = () => (
  <div>
    <Button size='sm' variant='primary'>
      小さいボタン
    </Button>
    <Button size='md' variant='secondary'>
      普通のボタン
    </Button>
    <Button size='lg' variant='outline'>
      大きいボタン
    </Button>
  </div>
);

カードコンポーネントでの活用

カードコンポーネントでも composition を活用できます。

javascript// カードの基本スタイル
const cardBase = css`
  background: white;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  transition: box-shadow 0.2s ease;
`;

// カードのサイズバリエーション
const cardSizes = {
  small: css`
    padding: 16px;
    max-width: 300px;
  `,
  medium: css`
    padding: 24px;
    max-width: 500px;
  `,
  large: css`
    padding: 32px;
    max-width: 800px;
  `,
};
javascript// カードのバリエーション
const cardVariants = {
  default: css`
    border: 1px solid #e5e7eb;
  `,
  elevated: css`
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);

    &:hover {
      box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
    }
  `,
  outlined: css`
    border: 2px solid #3b82f6;
    box-shadow: none;
  `,
};

// カードコンポーネント
const Card = styled.div`
  ${cardBase}
  ${(props) => cardSizes[props.size] || cardSizes.medium}
  ${(props) =>
    cardVariants[props.variant] || cardVariants.default}
`;

フォーム要素での活用

フォーム要素でも composition を活用して、一貫性のあるデザインを実現できます。

javascript// フォーム要素の基本スタイル
const formBase = css`
  border: 1px solid #d1d5db;
  border-radius: 6px;
  padding: 8px 12px;
  font-size: 16px;
  transition: border-color 0.2s ease;

  &:focus {
    outline: none;
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  }

  &:disabled {
    background: #f9fafb;
    cursor: not-allowed;
  }
`;

// エラー状態のスタイル
const errorState = css`
  border-color: #ef4444;

  &:focus {
    border-color: #ef4444;
    box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
  }
`;
javascript// 入力フィールドコンポーネント
const Input = styled.input`
  ${formBase}
  ${(props) => props.hasError && errorState}
`;

// テキストエリアコンポーネント
const TextArea = styled.textarea`
  ${formBase}
  ${(props) => props.hasError && errorState}
  resize: vertical;
  min-height: 100px;
`;

// セレクトボックスコンポーネント
const Select = styled.select`
  ${formBase}
  ${(props) => props.hasError && errorState}
  background: white;
`;

よくあるエラーと解決方法

エラー 1: css 関数のインポート忘れ

javascript// ❌ エラーが発生するコード
import styled from '@emotion/styled';

const baseStyles = css`
  color: red;
`;

// エラーメッセージ:
// ReferenceError: css is not defined
javascript// ✅ 正しいコード
import styled, { css } from '@emotion/styled';

const baseStyles = css`
  color: red;
`;

エラー 2: スタイルの適用順序の問題

javascript// ❌ 意図しない結果になるコード
const Button = styled.button`
  ${primaryButton}
  ${baseButtonStyles} // 後に書いたスタイルが優先される
`;

// 結果: baseButtonStyles の border-radius が primaryButton の値を上書き
javascript// ✅ 正しいコード
const Button = styled.button`
  ${baseButtonStyles}
  ${primaryButton} // 基本スタイルを先に、詳細スタイルを後に
`;

エラー 3: 条件付きスタイルでの未定義値

javascript// ❌ エラーが発生する可能性があるコード
const Button = styled.button`
  ${(props) =>
    buttonSizes[
      props.size
    ]}// props.size が undefined の場合エラー
`;
javascript// ✅ 安全なコード
const Button = styled.button`
  ${(props) => buttonSizes[props.size] || buttonSizes.md}
`;

まとめ

Emotion の composition を使いこなすためのポイントをまとめます。

基本原則

  1. 小さな部品から組み立てる

    • 基本スタイル、サイズ、色などを分離して定義
    • 再利用可能な小さなスタイルブロックを作成
  2. 命名規則を統一する

    • スタイル変数には説明的な名前を付ける
    • チーム内で一貫した命名規則を決める
  3. 条件付きスタイルは安全に

    • デフォルト値を必ず設定する
    • 未定義値に対する処理を忘れない

実践的なコツ

  1. テーマとの組み合わせ

    javascriptconst themedButton = css`
      ${baseButtonStyles}
      background: ${(props) => props.theme.colors.primary};
      color: ${(props) => props.theme.colors.white};
    `;
    
  2. メディアクエリとの組み合わせ

    javascriptconst responsiveButton = css`
      ${baseButtonStyles}
    
      @media (max-width: 768px) {
        padding: 8px 16px;
        font-size: 14px;
      }
    `;
    
  3. アニメーションとの組み合わせ

    javascriptconst animatedButton = css`
      ${baseButtonStyles}
      animation: ${fadeIn} 0.3s ease-in;
    `;
    

Emotion の composition をマスターすることで、保守性が高く、再利用可能なコンポーネントライブラリを構築できます。小さなスタイルの部品を組み合わせることで、大きな UI システムを作り上げる楽しさを実感してください。

最初は複雑に感じるかもしれませんが、実際に使ってみると、その柔軟性と強力さに驚くはずです。一歩ずつ進めていけば、必ず使いこなせるようになります。

関連リンク