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 | チーム開発 | 個人開発・小規模プロジェクト | 大規模プロジェクト・チーム開発 |
5 | TypeScript 連携 | シンプルなタイピング | 厳密な型安全性が必要 |
この基準を参考に、プロジェクトの特性に応じて選択していきましょう。
パフォーマンス観点での比較
パフォーマンスの観点から両 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;
`;
コードレビューでのチェックポイント
コードレビュー時には、以下の点を確認しましょう。
- 命名規則の統一: styled コンポーネントは PascalCase、css 関数で作成したスタイルは camelCase
- ファイル配置: 共通コンポーネントは専用ディレクトリに配置
- 再利用性の検討: 同じようなスタイルが複数箇所にある場合は、共通化を提案
具体例
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 を選択することで、開発効率と保守性を両立できます。
重要なのは、チーム内でルールを明確にし、一貫性のあるコードベースを維持することです。今回ご紹介した判断基準を参考に、プロジェクトに最適なスタイリング戦略を構築してください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来