T-CREATOR

Emotion 完全理解 2025:CSS-in-JS の強み・弱み・採用判断を徹底解説

Emotion 完全理解 2025:CSS-in-JS の強み・弱み・採用判断を徹底解説

近年のフロントエンド開発では、CSS-in-JS ライブラリの選択が開発効率やコードの保守性に大きな影響を与えています。特に React エコシステムにおいて、Emotion は TypeScript との親和性やパフォーマンス最適化の観点から多くの開発チームに選ばれている注目のライブラリです。

本記事では、Emotion の特徴から実際の導入判断まで、2025 年時点での最新情報を踏まえて包括的に解説いたします。CSS-in-JS ライブラリの選定に迷っている方や、Emotion の導入を検討している開発チームの方にとって、具体的な判断材料となる内容をお届けします。

背景

CSS-in-JS の台頭

従来の Web 開発では、HTML と CSS を分離して管理するのが一般的でした。しかし、この手法にはいくつかの課題が存在していました。

最も大きな問題はスコープの管理です。CSS はグローバルスコープで動作するため、異なるコンポーネント間でのスタイルの競合が頻繁に発生しました。また、大規模なプロジェクトになるほど、どの CSS ルールがどのコンポーネントに影響しているのかを把握することが困難になります。

mermaidflowchart TB
    global[グローバルCSS] --> comp1[コンポーネントA]
    global --> comp2[コンポーネントB]
    global --> comp3[コンポーネントC]
    comp1 -.-> conflict[スタイル競合]
    comp2 -.-> conflict
    comp3 -.-> conflict
    conflict --> maintenance[メンテナンス困難]

上図のように、グローバル CSS は複数のコンポーネントに影響を与え、予期しないスタイル競合を引き起こす可能性があります。

メンテナンス性の観点でも課題がありました。不要になった CSS ルールを特定・削除することが困難で、プロジェクトの成長とともに CSS ファイルが肥大化していく傾向がありました。

React 開発におけるスタイリング手法の変遷

React の普及とともに、コンポーネント志向の開発が主流となりました。この流れに合わせて、スタイリング手法も大きく変化してきました。

初期の React 開発では、インラインスタイルや CSS Modules が使われていました。しかし、これらの手法にも限界がありました。インラインスタイルでは疑似要素やメディアクエリが使用できず、CSS Modules では動的なスタイリングが困難でした。

mermaidflowchart LR
    inline[インラインスタイル] --> modules[CSS Modules]
    modules --> cssinjs[CSS-in-JS]
    cssinjs --> emotion[Emotion]
    cssinjs --> styled[styled-components]

    inline -.-> limit1[疑似要素未対応]
    modules -.-> limit2[動的スタイル困難]
    cssinjs --> solution[スコープ分離 + 動的対応]

React 開発におけるスタイリング手法の進化により、CSS-in-JS というアプローチが生まれました。これにより、JavaScript の力を借りてより柔軟で保守性の高いスタイリングが可能になったのです。

CSS-in-JS ライブラリの中でも、Emotion と Styled-components が特に注目を集めています。どちらもコンポーネントレベルでのスタイル管理を可能にし、JavaScript(TypeScript)との密な連携を実現しています。

Emotion の基本理解

Emotion とは

Emotion は、JavaScript 内で CSS を記述できる CSS-in-JS ライブラリです。React 専用ではありませんが、特に React エコシステムにおいて強力な機能を発揮します。

Emotion の設計思想は「パフォーマンスと開発体験の両立」にあります。コンパイル時の最適化とランタイムでの効率的な処理により、高いパフォーマンスを実現しながら、開発者にとって使いやすい API を提供しています。

typescriptimport { css } from '@emotion/react';

const buttonStyle = css`
  background-color: #007bff;
  color: white;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;

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

上記の例では、css関数を使ってスタイルを定義しています。このスタイルは自動的にユニークなクラス名が生成され、他のコンポーネントとの競合を防ぎます。

styled-components との違い

Emotion と Styled-components は、どちらも CSS-in-JS ライブラリとして人気ですが、いくつかの重要な違いがあります。

最も大きな違いは API の設計です。Emotion はcss prop という直感的なアプローチを提供し、より軽量で柔軟な実装が可能です。

比較項目Emotionstyled-components
バンドルサイズ軽量(約 7.9kB)やや大きい(約 12.6kB)
パフォーマンス高速標準的
API 設計css prop + styledstyled 中心
TypeScript 対応優秀良好
SSR サポート簡単設定が必要
typescript// Emotion のアプローチ
import { css } from '@emotion/react';

const MyComponent = ({ isActive }) => (
  <div
    css={css`
      color: ${isActive ? 'blue' : 'gray'};
      padding: 1rem;
    `}
  >
    コンテンツ
  </div>
);
typescript// styled-components のアプローチ
import styled from 'styled-components';

const StyledDiv = styled.div<{ isActive: boolean }>`
  color: ${(props) => (props.isActive ? 'blue' : 'gray')};
  padding: 1rem;
`;

const MyComponent = ({ isActive }) => (
  <StyledDiv isActive={isActive}>コンテンツ</StyledDiv>
);

Emotion の方がより直接的で、プロパティの受け渡しが簡潔に書けることがわかります。

主要な機能

Emotion は豊富な機能を提供しており、開発者のニーズに応じて適切な API を選択できます。

css prop

最も基本的で直感的な機能です。JSX 要素に直接スタイルを適用できます。

typescriptimport { css } from '@emotion/react';

const titleStyle = css`
  font-size: 2rem;
  font-weight: bold;
  margin-bottom: 1rem;
  color: #333;
`;

const Title = ({ children }) => (
  <h1 css={titleStyle}>{children}</h1>
);

css prop は条件分岐やプロパティに基づく動的なスタイリングも簡単に実現できます。

typescriptconst Button = ({ variant, disabled }) => (
  <button
    css={css`
      padding: 0.5rem 1rem;
      border: none;
      border-radius: 4px;
      cursor: ${disabled ? 'not-allowed' : 'pointer'};
      opacity: ${disabled ? 0.6 : 1};

      background-color: ${variant === 'primary'
        ? '#007bff'
        : '#6c757d'};
      color: white;

      &:hover:not(:disabled) {
        background-color: ${variant === 'primary'
          ? '#0056b3'
          : '#545b62'};
      }
    `}
    disabled={disabled}
  >
    ボタン
  </button>
);

styled API

Styled-components ライクな API も利用できます。再利用性の高いコンポーネントを作成する際に便利です。

typescriptimport styled from '@emotion/styled';

const Card = styled.div`
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  padding: 1.5rem;
  margin-bottom: 1rem;
`;

const CardTitle = styled.h2`
  margin: 0 0 1rem 0;
  font-size: 1.25rem;
  color: #333;
`;

型安全性を確保するために、TypeScript の型定義も簡単に行えます。

typescriptinterface ButtonProps {
  variant: 'primary' | 'secondary';
  size: 'small' | 'medium' | 'large';
}

const StyledButton = styled.button<ButtonProps>`
  padding: ${(props) => {
    switch (props.size) {
      case 'small':
        return '0.25rem 0.5rem';
      case 'large':
        return '0.75rem 1.5rem';
      default:
        return '0.5rem 1rem';
    }
  }};

  background-color: ${(props) =>
    props.variant === 'primary' ? '#007bff' : '#6c757d'};
`;

emotion/react vs emotion/styled

Emotion には主に 2 つのパッケージがあります。それぞれ異なる用途に最適化されています。

@emotion/reactは、css prop を中心とした軽量なアプローチを提供します。React プロジェクトでより直接的なスタイリングを行いたい場合に適しています。

@emotion/styledは、styled API を提供し、Styled-components からの移行やコンポーネントベースのスタイリングを好む場合に適しています。

typescript// @emotion/react を使用
import { css } from '@emotion/react';

const Component1 = () => (
  <div
    css={css`
      color: blue;
    `}
  >
    React package
  </div>
);
typescript// @emotion/styled を使用
import styled from '@emotion/styled';

const StyledDiv = styled.div`
  color: blue;
`;

const Component2 = () => (
  <StyledDiv>Styled package</StyledDiv>
);

両方のパッケージを組み合わせて使用することも可能で、プロジェクトの要件に応じて最適な組み合わせを選択できます。

強み(メリット)

開発体験の向上

TypeScript 完全対応

Emotion は TypeScript との親和性が非常に高く、型安全なスタイリングを実現できます。これにより、開発時のエラーを大幅に削減し、コードの品質向上に貢献します。

typescriptinterface ThemeProps {
  colors: {
    primary: string;
    secondary: string;
    danger: string;
  };
  spacing: {
    small: string;
    medium: string;
    large: string;
  };
}

const theme: ThemeProps = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    danger: '#dc3545',
  },
  spacing: {
    small: '0.5rem',
    medium: '1rem',
    large: '2rem',
  },
};

テーマオブジェクトに型を定義することで、スタイル記述時に型補完やエラーチェックが有効になります。

typescriptimport { css, Theme } from '@emotion/react';

const buttonStyles = (
  theme: ThemeProps,
  variant: keyof ThemeProps['colors']
) => css`
  background-color: ${theme.colors[variant]};
  padding: ${theme.spacing.medium};
  border: none;
  border-radius: 4px;
  color: white;
  cursor: pointer;

  &:hover {
    opacity: 0.8;
  }
`;

このように型を活用することで、存在しないプロパティの参照やタイポによるエラーを開発時点で発見できます。

優れたパフォーマンス

Emotion は複数の最適化技術により、高いパフォーマンスを実現しています。

自動的な CSS 最適化が行われ、同じスタイルルールは自動的に重複排除されます。これにより、最終的な CSS ファイルサイズを削減できます。

typescript// 同じスタイルが複数箇所で使用された場合
const baseButton = css`
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
`;

// Emotionが自動的に同じクラス名を再利用
const PrimaryButton = () => (
  <button css={baseButton}>Primary</button>
);
const SecondaryButton = () => (
  <button css={baseButton}>Secondary</button>
);

ビルド時の最適化により、不要なコードが削除され、本番環境でのバンドルサイズが最小化されます。

柔軟な API 設計

Emotion は開発者のニーズに応じて、複数の API パターンを提供しています。プロジェクトの要件やチームの好みに合わせて、適切なアプローチを選択できる柔軟性があります。

typescript// パターン1: css prop で直接記述
const DirectStyle = () => (
  <div
    css={css`
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    `}
  >
    直接スタイル
  </div>
);

// パターン2: css 関数で変数に格納
const centerStyle = css`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
`;

const VariableStyle = () => (
  <div css={centerStyle}>変数スタイル</div>
);

// パターン3: styled component として定義
const CenterContainer = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
`;

const StyledComponent = () => (
  <CenterContainer>Styledコンポーネント</CenterContainer>
);

実装上の利点

ランタイムサイズの最適化

Emotion は軽量な設計により、アプリケーションのランタイムサイズを最小限に抑えます。

mermaidflowchart LR
    emotion[Emotion<br/>7.9kB] --> app[アプリケーション]
    styled[styled-components<br/>12.6kB] --> app2[アプリケーション]

    app --> smaller[小さなバンドル]
    app2 --> larger[大きなバンドル]

    smaller --> fast[高速ロード]
    larger --> slow[低速ロード]

小さなバンドルサイズは、特にモバイル環境やネットワーク速度が制限される環境でのユーザー体験向上に直結します。

SSR サポート

Server-Side Rendering(SSR)のサポートが充実しており、Next.js などのフレームワークとの統合が簡単です。

typescript// pages/_document.tsx (Next.js)
import Document, {
  Html,
  Head,
  Main,
  NextScript,
} from 'next/document';
import { extractCritical } from '@emotion/server';

export default class MyDocument extends Document {
  static async getInitialProps(ctx: any) {
    const initialProps = await Document.getInitialProps(
      ctx
    );
    const critical = extractCritical(initialProps.html);
    initialProps.html = critical.html;
    initialProps.styles = (
      <>
        {initialProps.styles}
        <style
          data-emotion-css={critical.ids.join(' ')}
          dangerouslySetInnerHTML={{ __html: critical.css }}
        />
      </>
    );
    return initialProps;
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

この設定により、初回ページロードで必要な CSS のみが配信され、パフォーマンスが向上します。

テーマシステム

強力なテーマシステムにより、アプリケーション全体の一貫したデザインシステムを構築できます。

typescriptimport { ThemeProvider } from '@emotion/react';

const lightTheme = {
  colors: {
    background: '#ffffff',
    text: '#333333',
    primary: '#007bff',
  },
  breakpoints: {
    mobile: '768px',
    tablet: '1024px',
    desktop: '1200px',
  },
};

const darkTheme = {
  colors: {
    background: '#1a1a1a',
    text: '#ffffff',
    primary: '#66b3ff',
  },
  breakpoints: {
    mobile: '768px',
    tablet: '1024px',
    desktop: '1200px',
  },
};

const App = ({ isDarkMode }) => (
  <ThemeProvider
    theme={isDarkMode ? darkTheme : lightTheme}
  >
    <MainContent />
  </ThemeProvider>
);

コンポーネント内では、テーマの値を簡単に参照できます。

typescriptconst ThemedButton = () => (
  <button
    css={(theme) => css`
      background-color: ${theme.colors.primary};
      color: ${theme.colors.text};
      padding: 1rem 2rem;
      border: none;
      border-radius: 4px;

      @media (max-width: ${theme.breakpoints.mobile}) {
        padding: 0.5rem 1rem;
      }
    `}
  >
    テーマ対応ボタン
  </button>
);

弱み(デメリット)

導入時の課題

学習コストの存在

Emotion は CSS-in-JS の概念を理解する必要があり、従来の CSS に慣れた開発者にとって学習コストが発生します。

特に以下の概念について理解が必要です。

typescript// 1. css関数の使い方
const styles = css`
  color: blue;
  &:hover {
    color: red;
  }
`;

// 2. 動的スタイリング
const dynamicStyle = (isActive: boolean) => css`
  background: ${isActive ? 'blue' : 'gray'};
`;

// 3. テーマの概念
const themedStyle = (theme: any) => css`
  color: ${theme.colors.primary};
`;

チーム全体での導入を考える場合、適切な研修期間と学習サポートが必要になります。

ビルド設定の複雑さ

Emotion を最大限活用するためには、適切なビルド設定が必要です。特に Babel や TypeScript の設定で追加の作業が発生します。

javascript// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-react',
      {
        runtime: 'automatic',
        importSource: '@emotion/react',
      },
    ],
  ],
  plugins: ['@emotion/babel-plugin'],
};
json// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@emotion/react"
  }
}

これらの設定を間違えると、css prop が正しく動作しない場合があります。

運用上の注意点

ランタイムオーバーヘッド

CSS-in-JS ライブラリ全般に言えることですが、スタイルの生成と適用がランタイムで行われるため、わずかなオーバーヘッドが発生します。

mermaidflowchart TB
    render[コンポーネントレンダー] --> generate[スタイル生成]
    generate --> inject[DOM注入]
    inject --> display[表示]

    generate -.-> overhead[ランタイムオーバーヘッド]

パフォーマンスが重要なアプリケーションでは、この点を考慮する必要があります。

特に大量のコンポーネントを同時にレンダリングする場合は注意が必要です。

typescript// 問題のあるパターン:毎回新しいスタイルオブジェクトを生成
const BadExample = ({ items }) => (
  <div>
    {items.map((item) => (
      <div
        key={item.id}
        css={css`
          color: ${item.color};
          background: ${item.background};
        `}
      >
        {item.text}
      </div>
    ))}
  </div>
);

// 改善されたパターン:スタイルを事前に定義
const createItemStyle = (
  color: string,
  background: string
) => css`
  color: ${color};
  background: ${background};
`;

const GoodExample = ({ items }) => (
  <div>
    {items.map((item) => (
      <div
        key={item.id}
        css={createItemStyle(item.color, item.background)}
      >
        {item.text}
      </div>
    ))}
  </div>
);

デバッグの難しさ

従来の CSS とは異なり、生成されるクラス名が自動的に決まるため、ブラウザの開発者ツールでのデバッグが困難な場合があります。

css/* 生成されるクラス名の例 */
.css-1x2r8q4 {
  color: blue;
  padding: 1rem;
}

開発時にはラベル機能を活用してデバッグしやすくすることができます。

typescriptconst buttonStyle = css`
  label: primary-button;
  background-color: blue;
  color: white;
  padding: 1rem;
`;

これにより開発者ツールで「primary-button」として識別しやすくなります。

採用判断の指針

採用を推奨するケース

プロジェクト規模別判断

Emotion の採用は、プロジェクトの規模や特性によって適性が変わります。

小規模プロジェクト(~ 10 ページ程度)

  • 学習コストを考慮すると、従来の CSS や CSS Modules でも十分
  • ただし、TypeScript を使用し、デザインシステムを重視する場合はメリットあり

中規模プロジェクト(10 ~ 50 ページ程度)

  • Emotion の恩恵を最も受けやすい規模
  • コンポーネントの再利用性とメンテナンス性が重要になる段階
  • チーム規模も適度で、技術導入の合意形成がしやすい

大規模プロジェクト(50 ページ以上)

  • デザインシステムの一貫性が重要
  • 複数チームでの開発における統一性の確保
  • 長期的なメンテナンス性の観点で大きなメリット
mermaidgraph LR
    small[小規模<br/>~10ページ] --> css[CSS/CSS Modules<br/>推奨]
    medium[中規模<br/>10~50ページ] --> emotion[Emotion<br/>強く推奨]
    large[大規模<br/>50ページ以上] --> system[デザインシステム<br/>+ Emotion]

    small -.-> emotion_small[Emotion<br/>条件付き推奨]
    emotion_small -.-> ts[TypeScript使用]
    emotion_small -.-> design[デザインシステム重視]

チーム体制による選択

フロントエンドメインチーム

  • React/TypeScript に精通したメンバーが多い
  • 新しい技術への学習意欲が高い
  • Emotion の導入で開発効率が大幅に向上

フルスタック開発チーム

  • フロントエンド専任ではないメンバーも含む
  • 学習コストを抑えつつ、メリットを享受したい
  • 段階的な導入戦略が重要

外部委託を含むチーム

  • 技術的な統一が困難な場合がある
  • ドキュメント化と研修体制の整備が必要
  • 長期的な運用を考慮した技術選択が重要

代替手段との比較

Tailwind CSS

Tailwind CSS は、ユーティリティファーストのアプローチを取る CSS フレームワークです。

比較項目EmotionTailwind CSS
アプローチCSS-in-JSユーティリティクラス
学習コスト中程度低い
カスタマイズ性非常に高い高い
バンドルサイズ小さい最適化後は小さい
TypeScript 連携優秀普通
動的スタイリング簡単困難
typescript// Emotion
const Button = ({ variant, size }) => (
  <button
    css={css`
      background: ${variant === 'primary'
        ? 'blue'
        : 'gray'};
      padding: ${size === 'large'
        ? '1rem 2rem'
        : '0.5rem 1rem'};
    `}
  >
    ボタン
  </button>
);

// Tailwind CSS
const Button = ({ variant, size }) => (
  <button
    className={`
      ${
        variant === 'primary'
          ? 'bg-blue-500'
          : 'bg-gray-500'
      }
      ${size === 'large' ? 'px-8 py-4' : 'px-4 py-2'}
    `}
  >
    ボタン
  </button>
);

Tailwind CSS は学習コストが低く、すぐに始められる利点があります。一方、Emotion は動的なスタイリングや TypeScript との連携において優位性があります。

CSS Modules

CSS Modules は、従来の CSS にスコープ分離機能を追加したアプローチです。

css/* Button.module.css */
.button {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

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

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

const Button = ({ variant, children }) => (
  <button className={`${styles.button} ${styles[variant]}`}>
    {children}
  </button>
);

CSS Modules は従来の CSS 知識をそのまま活用できる利点がありますが、動的なスタイリングや TypeScript との連携では制限があります。

styled-components

styled-components は、Emotion と最も類似した CSS-in-JS ライブラリです。

typescript// styled-components
const StyledButton = styled.button<{ variant: string }>`
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  background-color: ${(props) =>
    props.variant === 'primary' ? '#007bff' : '#6c757d'};
`;

// Emotion
const Button = ({ variant }) => (
  <button
    css={css`
      padding: 0.5rem 1rem;
      border: none;
      border-radius: 4px;
      background-color: ${variant === 'primary'
        ? '#007bff'
        : '#6c757d'};
    `}
  >
    ボタン
  </button>
);

Emotion の方がバンドルサイズが小さく、パフォーマンスに優れています。また、css prop によるより直接的な記述が可能です。

具体例

実装パターン集

基本的な使用方法

Emotion の基本的な使用パターンを、よくある実装例とともに紹介します。

レスポンシブデザインの実装

typescriptimport { css } from '@emotion/react';

const responsiveContainer = css`
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1rem;

  @media (max-width: 768px) {
    padding: 0 0.5rem;
  }

  @media (max-width: 480px) {
    padding: 0 0.25rem;
  }
`;

const ResponsiveLayout = ({ children }) => (
  <div css={responsiveContainer}>{children}</div>
);

アニメーションの実装

typescriptimport { css, keyframes } from '@emotion/react';

const fadeIn = keyframes`
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;

const animatedCard = css`
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 2rem;
  animation: ${fadeIn} 0.3s ease-out;

  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
    transition: all 0.2s ease-out;
  }
`;

const AnimatedCard = ({ children }) => (
  <div css={animatedCard}>{children}</div>
);

フォームコンポーネントの実装

typescriptimport { css } from '@emotion/react';
import { useState } from 'react';

interface FormFieldProps {
  label: string;
  error?: string;
  required?: boolean;
}

const fieldContainer = css`
  margin-bottom: 1.5rem;
`;

const labelStyle = css`
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  color: #333;
`;

const inputStyle = (hasError: boolean) => css`
  width: 100%;
  padding: 0.75rem;
  border: 2px solid ${hasError ? '#dc3545' : '#ddd'};
  border-radius: 4px;
  font-size: 1rem;

  &:focus {
    outline: none;
    border-color: ${hasError ? '#dc3545' : '#007bff'};
    box-shadow: 0 0 0 3px ${hasError
        ? 'rgba(220, 53, 69, 0.1)'
        : 'rgba(0, 123, 255, 0.1)'};
  }
`;

const errorStyle = css`
  color: #dc3545;
  font-size: 0.875rem;
  margin-top: 0.25rem;
`;

const FormField: React.FC<FormFieldProps> = ({
  label,
  error,
  required,
  ...props
}) => {
  return (
    <div css={fieldContainer}>
      <label css={labelStyle}>
        {label}
        {required && (
          <span
            css={css`
              color: #dc3545;
            `}
          >
            *
          </span>
        )}
      </label>
      <input css={inputStyle(!!error)} {...props} />
      {error && <div css={errorStyle}>{error}</div>}
    </div>
  );
};

高度なテクニック

条件付きスタイルの効率的な管理

typescriptimport { css, SerializedStyles } from '@emotion/react';

interface ButtonStyleProps {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'small' | 'medium' | 'large';
  disabled?: boolean;
}

const baseButtonStyle = css`
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.2s ease;

  &:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
  }
`;

const variantStyles: Record<string, SerializedStyles> = {
  primary: css`
    background-color: #007bff;
    color: white;

    &:hover:not(:disabled) {
      background-color: #0056b3;
    }
  `,
  secondary: css`
    background-color: #6c757d;
    color: white;

    &:hover:not(:disabled) {
      background-color: #545b62;
    }
  `,
  danger: css`
    background-color: #dc3545;
    color: white;

    &:hover:not(:disabled) {
      background-color: #c82333;
    }
  `,
};

const sizeStyles: Record<string, SerializedStyles> = {
  small: css`
    padding: 0.25rem 0.5rem;
    font-size: 0.875rem;
  `,
  medium: css`
    padding: 0.5rem 1rem;
    font-size: 1rem;
  `,
  large: css`
    padding: 0.75rem 1.5rem;
    font-size: 1.125rem;
  `,
};

const disabledStyle = css`
  opacity: 0.6;
  cursor: not-allowed;
`;

const Button = ({
  variant,
  size,
  disabled,
  children,
  ...props
}: ButtonStyleProps) => (
  <button
    css={[
      baseButtonStyle,
      variantStyles[variant],
      sizeStyles[size],
      disabled && disabledStyle,
    ]}
    disabled={disabled}
    {...props}
  >
    {children}
  </button>
);

複雑なテーマシステムの実装

typescriptimport { css, Theme, ThemeProvider } from '@emotion/react';

interface AppTheme {
  colors: {
    primary: string;
    secondary: string;
    success: string;
    danger: string;
    warning: string;
    info: string;
    light: string;
    dark: string;
    background: string;
    surface: string;
    text: {
      primary: string;
      secondary: string;
      disabled: string;
    };
  };
  typography: {
    fontFamily: string;
    fontSize: {
      xs: string;
      sm: string;
      base: string;
      lg: string;
      xl: string;
      '2xl': string;
      '3xl': string;
    };
    fontWeight: {
      normal: number;
      medium: number;
      semibold: number;
      bold: number;
    };
  };
  spacing: {
    xs: string;
    sm: string;
    md: string;
    lg: string;
    xl: string;
    '2xl': string;
  };
  breakpoints: {
    sm: string;
    md: string;
    lg: string;
    xl: string;
  };
  shadows: {
    sm: string;
    md: string;
    lg: string;
    xl: string;
  };
}

const lightTheme: AppTheme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    success: '#28a745',
    danger: '#dc3545',
    warning: '#ffc107',
    info: '#17a2b8',
    light: '#f8f9fa',
    dark: '#343a40',
    background: '#ffffff',
    surface: '#f8f9fa',
    text: {
      primary: '#212529',
      secondary: '#6c757d',
      disabled: '#adb5bd',
    },
  },
  typography: {
    fontFamily:
      '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    fontSize: {
      xs: '0.75rem',
      sm: '0.875rem',
      base: '1rem',
      lg: '1.125rem',
      xl: '1.25rem',
      '2xl': '1.5rem',
      '3xl': '1.875rem',
    },
    fontWeight: {
      normal: 400,
      medium: 500,
      semibold: 600,
      bold: 700,
    },
  },
  spacing: {
    xs: '0.25rem',
    sm: '0.5rem',
    md: '1rem',
    lg: '1.5rem',
    xl: '2rem',
    '2xl': '3rem',
  },
  breakpoints: {
    sm: '640px',
    md: '768px',
    lg: '1024px',
    xl: '1280px',
  },
  shadows: {
    sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
    md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
    lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
    xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
  },
};

// テーマを使用したコンポーネント
const ThemedCard = ({ children }) => (
  <div
    css={(theme: AppTheme) => css`
      background: ${theme.colors.surface};
      border-radius: 8px;
      box-shadow: ${theme.shadows.md};
      padding: ${theme.spacing.lg};
      margin-bottom: ${theme.spacing.md};

      @media (max-width: ${theme.breakpoints.md}) {
        padding: ${theme.spacing.md};
      }
    `}
  >
    {children}
  </div>
);

パフォーマンス最適化

メモ化を活用したスタイル最適化

typescriptimport { css } from '@emotion/react';
import { useMemo } from 'react';

interface OptimizedComponentProps {
  items: Array<{
    id: string;
    color: string;
    size: number;
  }>;
}

const OptimizedComponent = ({
  items,
}: OptimizedComponentProps) => {
  // スタイルをメモ化して再計算を防ぐ
  const itemStyles = useMemo(() => {
    return items.reduce((acc, item) => {
      acc[item.id] = css`
        color: ${item.color};
        font-size: ${item.size}px;
        padding: 0.5rem;
        margin-bottom: 0.25rem;
        border-radius: 4px;
        transition: all 0.2s ease;

        &:hover {
          transform: scale(1.02);
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }
      `;
      return acc;
    }, {} as Record<string, any>);
  }, [items]);

  return (
    <div>
      {items.map((item) => (
        <div key={item.id} css={itemStyles[item.id]}>
          {item.id}
        </div>
      ))}
    </div>
  );
};

クリティカル CSS の生成と注入

typescript// utils/critical-css.ts
import { extractCritical } from '@emotion/server';
import { renderToString } from 'react-dom/server';

export const generateCriticalCSS = (
  component: React.ReactElement
) => {
  const { html, css, ids } = extractCritical(
    renderToString(component)
  );

  return {
    html,
    css,
    ids,
  };
};

// pages/_document.tsx (Next.js)
import Document, {
  Html,
  Head,
  Main,
  NextScript,
} from 'next/document';
import { generateCriticalCSS } from '../utils/critical-css';

export default class MyDocument extends Document {
  static async getInitialProps(ctx: any) {
    const initialProps = await Document.getInitialProps(
      ctx
    );

    // クリティカルCSSの生成
    const critical = extractCritical(initialProps.html);

    return {
      ...initialProps,
      styles: (
        <>
          {initialProps.styles}
          <style
            data-emotion-css={critical.ids.join(' ')}
            dangerouslySetInnerHTML={{
              __html: critical.css,
            }}
          />
        </>
      ),
    };
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

バンドル分割とコード分離

typescript// components/LazyStyledComponent.tsx
import { lazy, Suspense } from 'react';
import { css } from '@emotion/react';

// 重いスタイルコンポーネントの遅延読み込み
const HeavyStyledComponent = lazy(
  () => import('./HeavyStyledComponent')
);

const loadingStyle = css`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
  background: #f8f9fa;
  border-radius: 8px;
`;

const LoadingFallback = () => (
  <div css={loadingStyle}>読み込み中...</div>
);

const LazyStyledComponent = () => (
  <Suspense fallback={<LoadingFallback />}>
    <HeavyStyledComponent />
  </Suspense>
);

export default LazyStyledComponent;

これらの実装パターンを参考に、プロジェクトの要件に応じて適切なアプローチを選択してください。パフォーマンス最適化については、実際の使用状況を測定しながら段階的に適用することが重要です。

まとめ

Emotion は、現代の React 開発において強力な選択肢となる CSS-in-JS ライブラリです。TypeScript との優れた親和性、軽量な設計、柔軟な API、そして充実したパフォーマンス最適化機能により、多くの開発チームで採用されています。

特に中規模以上のプロジェクトにおいて、コンポーネント指向の開発スタイルとマッチし、長期的なメンテナンス性の向上に大きく貢献します。デザインシステムの構築やテーマシステムの実装においても、その真価を発揮するでしょう。

一方で、学習コストやビルド設定の複雑さなど、導入時に考慮すべき課題も存在します。チームの技術レベルやプロジェクトの要件を十分に検討した上で、段階的な導入を進めることが成功の鍵となります。

2025 年の現在において、Emotion は技術的に成熟し、エコシステムも充実しています。CSS-in-JS ライブラリの選択に迷っている場合は、その優れたパフォーマンスと開発体験を体験してみることをお勧めします。適切に活用すれば、開発効率とコード品質の両面で大きなメリットを得られるはずです。

関連リンク