T-CREATOR

Emotion の css 関数と styled API の使い分け

Emotion の css 関数と styled API の使い分け

React 開発において、Emotion は最も人気の高い CSS-in-JS ライブラリの一つです。

Emotion には主に 2 つのスタイリング手法があります。それがcss関数とstyled API です。どちらも強力な機能を持っていますが、適切な使い分けができていないと、コードの可読性や保守性に問題が生じることがあります。

今回は、この 2 つの API の特徴を理解し、プロジェクトに応じた最適な選択ができるよう、実践的な判断基準をご紹介します。

背景

React 開発におけるスタイリング手法の多様化

モダンな React 開発では、スタイリング手法が多様化しています。

従来の CSS、CSS Modules、Sass、そして CSS-in-JS ライブラリなど、選択肢が豊富になった一方で、どの手法を選ぶべきか迷うケースも増えました。特に、コンポーネントベースの開発では、スタイルとロジックの密結合が求められる場面が多くなっています。

CSS-in-JS ライブラリの中でも、Emotion は高いパフォーマンスと柔軟性を兼ね備えており、多くの開発者に選ばれています。

Emotion の 2 つの主要 API の存在

Emotion には主要な API が 2 つ存在します。

css関数は、文字列やオブジェクト形式でスタイルを定義し、クラス名として使用する手法です。一方、styled API は、既存の HTML 要素や React コンポーネントにスタイルを適用した新しいコンポーネントを作成する手法になります。

どちらも同じ結果を実現できる場合が多いのですが、それぞれに異なる特性があり、使い分けることでより効率的な開発が可能になります。

課題

css 関数と styled API の使い分けが不明確

多くの開発者が直面する問題として、「どちらを使えばよいのかわからない」という点があります。

公式ドキュメントでは両方の使い方は説明されていますが、具体的にどのような場面でどちらを選ぶべきかの明確な指針は示されていません。そのため、開発者の好みや経験に依存して選択されることが多く、プロジェクト内で一貫性が取れない場合があります。

また、途中で API を変更したくなった場合の移行コストも考慮する必要があります。

開発効率とメンテナンス性のバランス

開発効率を重視すると、書きやすい方法を選びがちです。

しかし、長期的なメンテナンス性を考慮すると、コードの可読性や再利用性も重要になります。css関数は簡潔に書けることが多い一方で、styled API はコンポーネントとして管理できるため、再利用性に優れています。

このバランスをどう取るかは、プロジェクトの規模や開発チームの特性によって異なります。

プロジェクトの一貫性確保の困難

大規模なプロジェクトや複数人での開発では、スタイリング手法の一貫性確保が重要です。

開発者によって異なる API を使用していると、コードレビューが困難になったり、新しいメンバーが参加した際の学習コストが高くなったりします。また、リファクタリング時にも統一されていない手法が障壁となることがあります。

解決策

判断基準の明確化

適切な使い分けを行うためには、明確な判断基準を設けることが重要です。

以下の表で、主要な判断ポイントを整理しました。

#判断基準css 関数が適しているstyled API が適している
1使用頻度一時的・局所的なスタイル再利用可能なコンポーネント
2動的スタイル条件によるスタイル変更プロップスベースのスタイル変更
3コンポーネント設計既存コンポーネントの装飾新規コンポーネント作成
4チーム開発個人開発・小規模プロジェクト大規模プロジェクト・チーム開発
5TypeScript 連携シンプルなタイピング厳密な型安全性が必要

この基準を参考に、プロジェクトの特性に応じて選択していきましょう。

パフォーマンス観点での比較

パフォーマンスの観点から両 API を比較すると、いくつかの違いがあります。

css関数は、スタイルオブジェクトを直接クラス名に変換するため、ランタイムでのオーバーヘッドが比較的少なくなります。一方、styled API は、新しいコンポーネントを作成するため、わずかながらメモリ使用量が増加します。

ただし、実際のアプリケーションでは、この差が体感できるほど大きくなることは稀です。

バンドルサイズへの影響

javascript// css関数を使用した場合のインポート
import { css } from '@emotion/react';

// styled APIを使用した場合のインポート
import styled from '@emotion/styled';

styled API を使用する場合、追加のライブラリをインポートする必要があるため、バンドルサイズがわずかに増加します。しかし、現代のバンドラーでは適切に Tree Shaking が行われるため、使用していない部分は除去されます。

レンダリングパフォーマンス

javascript// css関数:毎回新しいクラス名を生成
const dynamicStyle = css`
  color: ${props.isActive ? 'blue' : 'gray'};
  font-size: ${props.size}px;
`;

// styled API:メモ化により最適化される場合がある
const DynamicButton = styled.button`
  color: ${(props) => (props.isActive ? 'blue' : 'gray')};
  font-size: ${(props) => props.size}px;
`;

動的なスタイルの場合、styled API の方が内部的なメモ化により、パフォーマンスが向上することがあります。

チーム開発における統一ルール

チーム開発では、以下のようなルールを設定することをお勧めします。

基本方針の設定

typescript// チーム開発での基本方針例

// 1. 再利用可能なコンポーネントはstyled APIを使用
const Button = styled.button<{
  variant: 'primary' | 'secondary';
}>`
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  background-color: ${(props) =>
    props.variant === 'primary' ? '#007bff' : '#6c757d'};
  color: white;
  cursor: pointer;
`;

// 2. 一時的なスタイルはcss関数を使用
const tempStyle = css`
  margin-top: 20px;
  text-align: center;
`;

コードレビューでのチェックポイント

コードレビュー時には、以下の点を確認しましょう。

  1. 命名規則の統一: styled コンポーネントは PascalCase、css 関数で作成したスタイルは camelCase
  2. ファイル配置: 共通コンポーネントは専用ディレクトリに配置
  3. 再利用性の検討: 同じようなスタイルが複数箇所にある場合は、共通化を提案

具体例

css 関数が適している場面

ケース 1: 条件付きスタイルの適用

条件によってスタイルを変更したい場合、css関数が直感的で効率的です。

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

interface MessageProps {
  type: 'success' | 'error' | 'warning';
  children: React.ReactNode;
}
typescript// メッセージタイプに応じたスタイルを定義
const getMessageStyle = (type: string) => {
  const baseStyle = css`
    padding: 12px;
    border-radius: 4px;
    margin: 8px 0;
  `;

  const typeStyles = {
    success: css`
      background-color: #d4edda;
      border: 1px solid #c3e6cb;
      color: #155724;
    `,
    error: css`
      background-color: #f8d7da;
      border: 1px solid #f5c6cb;
      color: #721c24;
    `,
    warning: css`
      background-color: #fff3cd;
      border: 1px solid #ffeaa7;
      color: #856404;
    `,
  };

  return [baseStyle, typeStyles[type]];
};
typescript// コンポーネントでの使用
const Message: React.FC<MessageProps> = ({
  type,
  children,
}) => {
  return <div css={getMessageStyle(type)}>{children}</div>;
};

// 使用例
const App = () => {
  return (
    <div>
      <Message type='success'>処理が完了しました</Message>
      <Message type='error'>エラーが発生しました</Message>
      <Message type='warning'>注意が必要です</Message>
    </div>
  );
};

この例では、メッセージタイプに応じて動的にスタイルを切り替えています。css関数を使うことで、条件分岐が明確になり、保守しやすいコードになります。

ケース 2: アニメーションの実装

アニメーション効果を適用する際も、css関数が便利です。

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

// キーフレームアニメーションの定義
const fadeIn = keyframes`
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;
typescript// アニメーションスタイルの適用
const animatedCardStyle = css`
  animation: ${fadeIn} 0.3s ease-out;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin: 16px 0;
`;

const AnimatedCard: React.FC<{
  children: React.ReactNode;
}> = ({ children }) => {
  return <div css={animatedCardStyle}>{children}</div>;
};

styled API が適している場面

ケース 1: 再利用可能な UI コンポーネント

デザインシステムで使用する基本的な UI コンポーネントは、styled API が最適です。

typescriptimport styled from '@emotion/styled';

// ボタンコンポーネントの型定義
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'outline';
  size?: 'small' | 'medium' | 'large';
  fullWidth?: boolean;
  disabled?: boolean;
}
typescript// サイズとバリエーションに応じたスタイル関数
const getButtonSize = (size: string) => {
  const sizes = {
    small: 'padding: 4px 8px; font-size: 12px;',
    medium: 'padding: 8px 16px; font-size: 14px;',
    large: 'padding: 12px 24px; font-size: 16px;',
  };
  return sizes[size] || sizes.medium;
};

const getButtonVariant = (variant: string) => {
  const variants = {
    primary: `
      background-color: #007bff;
      color: white;
      border: 1px solid #007bff;
      &:hover { background-color: #0056b3; }
    `,
    secondary: `
      background-color: #6c757d;
      color: white;
      border: 1px solid #6c757d;
      &:hover { background-color: #545b62; }
    `,
    outline: `
      background-color: transparent;
      color: #007bff;
      border: 1px solid #007bff;
      &:hover { background-color: #007bff; color: white; }
    `,
  };
  return variants[variant] || variants.primary;
};
typescript// メインのButtonコンポーネント
const Button = styled.button<ButtonProps>`
  ${(props) => getButtonSize(props.size || 'medium')}
  ${(props) => getButtonVariant(props.variant || 'primary')}
  
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s ease-in-out;
  font-weight: 500;

  ${(props) => props.fullWidth && 'width: 100%;'}

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

    &:hover {
      transform: none;
    }
  }

  &:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
  }
`;
typescript// 使用例
const ButtonExample = () => {
  return (
    <div>
      <Button variant='primary' size='large'>
        メインボタン
      </Button>
      <Button variant='secondary' size='medium'>
        サブボタン
      </Button>
      <Button variant='outline' size='small' fullWidth>
        フルワイドボタン
      </Button>
      <Button disabled>無効化ボタン</Button>
    </div>
  );
};

この例では、プロップスに基づいて動的にスタイルが変化する Button コンポーネントを作成しています。styled API を使うことで、TypeScript の型安全性を保ちながら、再利用可能なコンポーネントが作成できます。

ケース 2: 既存コンポーネントの拡張

既存のコンポーネントを拡張して、新しいスタイルを適用したい場合にも便利です。

typescript// 基本のInputコンポーネント
const BaseInput = styled.input`
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;

  &:focus {
    outline: none;
    border-color: #007bff;
    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
  }
`;
typescript// 検索用のInputコンポーネント(アイコン付き)
const SearchInput = styled(BaseInput)`
  padding-left: 36px;
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="%23666" viewBox="0 0 16 16"><path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/></svg>');
  background-repeat: no-repeat;
  background-position: 12px center;
  background-size: 16px 16px;
`;

// エラー状態のInputコンポーネント
const ErrorInput = styled(BaseInput)`
  border-color: #dc3545;

  &:focus {
    border-color: #dc3545;
    box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1);
  }
`;

ハイブリッドアプローチ

実際のプロジェクトでは、両方の API を組み合わせて使用することが多くあります。

ケース 1: styled コンポーネント + 動的 css

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

// ベースのCardコンポーネント
const Card = styled.div`
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin: 16px 0;
`;
typescript// 状態に応じた追加スタイル
const getCardStateStyle = (
  isHighlighted: boolean,
  isClickable: boolean
) => {
  return css`
    ${isHighlighted &&
    `
      border: 2px solid #007bff;
      box-shadow: 0 4px 20px rgba(0, 123, 255, 0.2);
    `}

    ${isClickable &&
    `
      cursor: pointer;
      transition: transform 0.2s ease-in-out;
      
      &:hover {
        transform: translateY(-2px);
      }
    `}
  `;
};
typescript// ハイブリッドアプローチでの使用
interface InteractiveCardProps {
  isHighlighted?: boolean;
  isClickable?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
}

const InteractiveCard: React.FC<InteractiveCardProps> = ({
  isHighlighted = false,
  isClickable = false,
  onClick,
  children,
}) => {
  return (
    <Card
      css={getCardStateStyle(isHighlighted, isClickable)}
      onClick={isClickable ? onClick : undefined}
    >
      {children}
    </Card>
  );
};

ケース 2: テーマベースのスタイリング

typescript// テーマの型定義
interface Theme {
  colors: {
    primary: string;
    secondary: string;
    background: string;
    text: string;
  };
  spacing: {
    small: string;
    medium: string;
    large: string;
  };
}
typescript// テーマを使用したstyledコンポーネント
const ThemedButton = styled.button`
  background-color: ${(props) =>
    props.theme.colors.primary};
  color: white;
  padding: ${(props) => props.theme.spacing.medium};
  border: none;
  border-radius: 4px;
  cursor: pointer;
`;
typescript// css関数でのテーマ使用
const getThemedCardStyle = (
  theme: Theme,
  variant: 'light' | 'dark'
) => css`
  background-color: ${variant === 'light'
    ? theme.colors.background
    : theme.colors.text};
  color: ${variant === 'light'
    ? theme.colors.text
    : theme.colors.background};
  padding: ${theme.spacing.large};
  border-radius: 8px;
  margin: ${theme.spacing.medium} 0;
`;

// 使用例
const ThemedCard: React.FC<{
  variant: 'light' | 'dark';
  children: React.ReactNode;
}> = ({ variant, children, ...props }) => {
  const theme = useTheme(); // テーマフックの使用

  return (
    <div
      css={getThemedCardStyle(theme, variant)}
      {...props}
    >
      {children}
    </div>
  );
};

まとめ

Emotion のcss関数とstyled API は、それぞれ異なる強みを持っています。

css関数は、条件付きスタイリングや一時的なスタイル適用に向いており、シンプルで直感的な記述ができます。一方、styled API は、再利用可能なコンポーネント作成や TypeScript との連携において優れた能力を発揮します。

最適な選択をするためには、以下のポイントを考慮することが重要です。

  • プロジェクトの規模と複雑さ:小規模なプロジェクトではcss関数が効率的、大規模なプロジェクトではstyled API が管理しやすい
  • チーム開発の有無:チーム開発ではstyled API の方が一貫性を保ちやすい
  • 再利用性の要求:デザインシステムを構築する場合はstyled API、局所的なスタイリングはcss関数
  • TypeScript との連携:型安全性を重視する場合はstyled API が有利

また、実際のプロジェクトでは、どちらか一方に固執する必要はありません。ハイブリッドアプローチを採用し、場面に応じて最適な API を選択することで、開発効率と保守性を両立できます。

重要なのは、チーム内でルールを明確にし、一貫性のあるコードベースを維持することです。今回ご紹介した判断基準を参考に、プロジェクトに最適なスタイリング戦略を構築してください。

関連リンク