styled-components と Emotion の違いを徹底比較

React 開発において、コンポーネントのスタイリングは避けて通れない重要な要素です。従来の CSS ファイルでのスタイリングから進化し、現在多くの開発者が CSS-in-JS ライブラリを採用しています。
特にstyled-componentsとEmotionは、React エコシステムにおける二大巨頭として君臨し、多くのプロジェクトで採用されています。しかし、どちらを選ぶべきか迷われている方も多いのではないでしょうか。
この記事では、両者の違いを実際のコード例やエラー事例を交えながら徹底的に比較し、あなたのプロジェクトに最適な選択肢を見つけるお手伝いをいたします。
背景 - CSS-in-JS ライブラリが注目される理由
従来のスタイリング手法の課題
Web 開発の歴史を振り返ると、スタイリングは常に開発者を悩ませる課題でした。従来の CSS ファイルによるスタイリングでは、以下のような問題が頻繁に発生していました。
# | 課題 | 具体的な問題 |
---|---|---|
1 | グローバルスコープ | クラス名の衝突による予期しないスタイル適用 |
2 | デッドコード | 使われなくなった CSS の特定困難 |
3 | 動的スタイリング | Props に基づくスタイル変更の複雑さ |
4 | メンテナンス性 | コンポーネントとスタイルの分離による保守困難 |
CSS-in-JS が解決する本質的価値
CSS-in-JS ライブラリは、これらの課題を根本から解決します。最も重要なのは、スタイルとロジックの結合による開発体験の劇的な向上です。
typescript// 従来のCSS + Reactの課題例
import './Button.css'; // 別ファイル管理が必要
const Button = ({ variant, disabled }) => {
// 複雑な条件分岐でクラス名を管理
const className = `button ${
variant === 'primary'
? 'button--primary'
: 'button--secondary'
} ${disabled ? 'button--disabled' : ''}`;
return <button className={className}>Click me</button>;
};
この従来手法では、スタイルとコンポーネントが分離されているため、変更時に複数ファイルを編集する必要があり、開発効率が下がってしまいます。
styled-components と Emotion の基本概要
styled-components - 老舗の安定感
styled-components は 2016 年にリリースされた、CSS-in-JS ライブラリの先駆者的存在です。その最大の特徴は、テンプレートリテラルを使った直感的な記法にあります。
typescriptimport styled from 'styled-components';
const Button = styled.button`
background-color: ${(props) =>
props.primary ? '#007bff' : '#6c757d'};
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
この記法の美しさは、CSS そのものの構文を保ちながら、JavaScript の動的な値を自然に組み込める点です。まるで従来の CSS を書いているような感覚で、Props ベースの動的スタイリングが実現できます。
Emotion - 革新的なパフォーマンス志向
Emotion は 2017 年にリリースされ、styled-components の良さを継承しつつ、パフォーマンスとバンドルサイズの最適化に重点を置いて開発されました。
typescriptimport styled from '@emotion/styled';
import { css } from '@emotion/react';
// styled記法
const Button = styled.button`
background-color: ${(props) =>
props.primary ? '#007bff' : '#6c757d'};
color: white;
`;
// css記法
const buttonStyle = css`
background-color: #007bff;
color: white;
`;
Emotion の革新的な点は、複数の記法を提供している柔軟性です。プロジェクトの要件や開発者の好みに応じて、最適なアプローチを選択できます。
記法・書き方の違い
テンプレートリテラル記法の比較
両ライブラリとも基本的なテンプレートリテラル記法は非常に似ていますが、細かな違いが存在します。
styled-components の記法例:
typescriptimport styled, { css } from 'styled-components';
const Container = styled.div`
padding: 20px;
${(props) =>
props.centered &&
css`
display: flex;
justify-content: center;
align-items: center;
`}
`;
// 使用例
<Container centered>
<p>中央配置されたコンテンツ</p>
</Container>;
Emotion の記法例:
typescriptimport styled from '@emotion/styled';
import { css } from '@emotion/react';
const Container = styled.div`
padding: 20px;
${(props) =>
props.centered &&
css`
display: flex;
justify-content: center;
align-items: center;
`}
`;
// 追加:css記法も利用可能
const centerStyle = css`
display: flex;
justify-content: center;
align-items: center;
`;
css 記法での違いと活用場面
Emotion の大きな特徴の一つが、css
記法の柔軟な活用です。この記法は、スタイルオブジェクトを変数として管理したい場合に威力を発揮します。
typescriptimport { css } from '@emotion/react';
// スタイルを変数として定義
const baseButtonStyle = css`
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
`;
const primaryButtonStyle = css`
${baseButtonStyle}
background-color: #007bff;
color: white;
&:hover {
background-color: #0056b3;
}
`;
// JSX内で直接使用
const MyComponent = () => (
<button css={primaryButtonStyle}>
クリックしてください
</button>
);
このcss
記法は、スタイルの再利用性を高め、より宣言的なコンポーネント設計を可能にします。
実際に発生するエラーと対処法
開発中によく遭遇するエラーをご紹介します。これらのエラーメッセージで検索される方も多いでしょう。
styled-components でよくあるエラー:
bash# エラーメッセージ
TypeError: Cannot read property 'attrs' of undefined
# 原因:importの間違い
import styled from 'styled-components/macro'; // ❌ 間違い
import styled from 'styled-components'; // ✅ 正しい
Emotion でよくあるエラー:
bash# エラーメッセージ
Module parse failed: Unexpected token
You may need an appropriate loader to handle this file type
# 原因:@emotion/babel-plugin の設定不足
# babel.config.js に以下を追加
{
"plugins": ["@emotion/babel-plugin"]
}
パフォーマンス比較
ランタイムパフォーマンス
パフォーマンスは現代の Web 開発において最も重要な指標の一つです。両ライブラリのランタイムパフォーマンスを詳しく見てみましょう。
# | 項目 | styled-components | Emotion |
---|---|---|---|
1 | 初回レンダリング | 中程度 | 高速 |
2 | 再レンダリング | 中程度 | 高速 |
3 | CSS 生成速度 | 中程度 | 高速 |
4 | メモリ使用量 | やや多め | 効率的 |
実際のベンチマークテストコード:
typescript// パフォーマンステスト用のコンポーネント
import { performance } from 'perf_hooks';
import styled from 'styled-components';
// import styled from '@emotion/styled'; // 比較用
const TestButton = styled.button`
background: ${(props) => props.color || 'blue'};
padding: 10px;
margin: 5px;
`;
// レンダリング時間計測
const measureRenderTime = () => {
const start = performance.now();
// 1000個のボタンをレンダリング
const buttons = Array.from({ length: 1000 }, (_, i) => (
<TestButton
key={i}
color={`hsl(${i * 0.36}, 70%, 50%)`}
>
Button {i}
</TestButton>
));
const end = performance.now();
console.log(`レンダリング時間: ${end - start}ms`);
};
ビルドタイムでの最適化
Emotion は、ビルドタイムでの最適化により、より効率的な CSS 生成を実現しています。
typescript// Emotion のビルドタイム最適化例
import { css } from '@emotion/react';
// 開発時のコード
const dynamicStyle = css`
color: ${(props) => props.theme.primary};
font-size: ${(props) => props.size}px;
`;
// ビルド後に最適化される
// → 静的な部分は事前にコンパイルされ、動的な部分のみランタイムで処理
バンドルサイズとインストール方法
バンドルサイズの比較
アプリケーションの読み込み速度に直結するバンドルサイズは、ライブラリ選択の重要な判断材料です。
# | ライブラリ | ミニファイド版 | Gzip 圧縮後 |
---|---|---|---|
1 | styled-components | 76.8KB | 19.8KB |
2 | @emotion/styled + @emotion/react | 42.6KB | 12.4KB |
3 | @emotion/styled のみ | 28.1KB | 8.9KB |
Emotion の方が明らかに軽量で、特に@emotion/styled
のみを使用する場合は、大幅にバンドルサイズを削減できます。
インストール方法と初期設定
styled-components のインストール:
bash# 基本インストール
yarn add styled-components
yarn add --dev @types/styled-components
# TypeScript用の設定
# styled-components.d.ts
import 'styled-components';
declare module 'styled-components' {
export interface DefaultTheme {
colors: {
primary: string;
secondary: string;
};
}
}
Emotion のインストール:
bash# 基本パッケージ
yarn add @emotion/react @emotion/styled
# Babel設定(推奨)
yarn add --dev @emotion/babel-plugin
# Next.js用の設定
yarn add @emotion/server
Next.js での設定差分
Next.js 環境での設定方法も、両者で微妙に異なります。
styled-components + Next.js:
typescript// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
styledComponents: true,
},
};
module.exports = nextConfig;
Emotion + Next.js:
typescript// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 特別な設定は不要(babel-pluginで対応)
};
// babel.config.js
module.exports = {
presets: ['next/babel'],
plugins: ['@emotion/babel-plugin'],
};
TypeScript 対応状況
型安全性の比較
TypeScript での開発体験は、モダンな React 開発において欠かせない要素です。両ライブラリの型対応を詳しく見てみましょう。
styled-components の TypeScript 対応:
typescriptimport styled from 'styled-components';
// Propsの型定義
interface ButtonProps {
variant: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
}
const Button = styled.button<ButtonProps>`
background-color: ${({ variant }) =>
variant === 'primary' ? '#007bff' : '#6c757d'
};
padding: ${({ size = 'medium' }) => {
switch (size) {
case 'small': return '8px 16px';
case 'large': return '16px 32px';
default: return '12px 24px';
}
}};
opacity: ${({ disabled }) => disabled ? 0.6 : 1};
cursor: ${({ disabled }) => disabled ? 'not-allowed' : 'pointer'};
`;
// 使用時に型チェックが働く
<Button variant="primary" size="large" /> // ✅ OK
<Button variant="danger" /> // ❌ 型エラー
Emotion の TypeScript 対応:
typescriptimport styled from '@emotion/styled';
import { css, SerializedStyles } from '@emotion/react';
interface ButtonProps {
variant: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
}
const Button = styled.button<ButtonProps>`
background-color: ${({ variant }) =>
variant === 'primary' ? '#007bff' : '#6c757d'};
`;
// css記法での型安全性
const getButtonSize = (
size: ButtonProps['size']
): SerializedStyles => css`
padding: ${size === 'small'
? '8px 16px'
: size === 'large'
? '16px 32px'
: '12px 24px'};
`;
よくある型エラーと解決法
開発中に遭遇する型エラーの事例をご紹介します。
typescript// よくあるエラー:テーマの型が推論されない
const Button = styled.button`
color: ${(props) =>
props.theme.colors.primary}; // ❌ 型エラー
`;
// 解決法:DefaultTheme を拡張
declare module 'styled-components' {
export interface DefaultTheme {
colors: {
primary: string;
secondary: string;
};
}
}
// または、型アサーション
const Button = styled.button`
color: ${(props: { theme: DefaultTheme }) =>
props.theme.colors.primary};
`;
コミュニティとエコシステム
GitHub 統計とメンテナンス状況
両ライブラリのコミュニティ活動を数値で比較してみましょう。
# | 指標 | styled-components | Emotion |
---|---|---|---|
1 | GitHub スター数 | 40,000+ | 17,000+ |
2 | 週間ダウンロード数 | 4,500,000 | 5,200,000 |
3 | コントリビューター数 | 300+ | 200+ |
4 | 最終リリース | 継続的 | 継続的 |
興味深いことに、styled-components の方がスター数は多いものの、Emotion の方がダウンロード数では上回っています。これは、近年のパフォーマンス重視の傾向を反映しているかもしれません。
サードパーティライブラリとの統合
styled-components のエコシステム:
typescript// Polished - styled-components用のユーティリティ
import { darken, lighten } from 'polished';
import styled from 'styled-components';
const Button = styled.button`
background-color: #007bff;
&:hover {
background-color: ${darken(0.1, '#007bff')};
}
&:active {
background-color: ${darken(0.2, '#007bff')};
}
`;
Emotion のエコシステム:
typescript// @emotion/css - ユーティリティ関数群
import { css, keyframes } from '@emotion/react';
const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;
const animatedStyle = css`
animation: ${fadeIn} 0.3s ease-in-out;
`;
具体的な実装例での比較
実際のプロダクトレベルの比較
実際のアプリケーション開発でよく使われるパターンを両ライブラリで実装して比較してみましょう。
テーマを活用したダークモード対応コンポーネント:
typescript// styled-components版
import styled, {
ThemeProvider,
createGlobalStyle,
} from 'styled-components';
const GlobalStyle = createGlobalStyle`
body {
background-color: ${(props) =>
props.theme.backgroundColor};
color: ${(props) => props.theme.textColor};
transition: all 0.3s ease;
}
`;
const Card = styled.div`
background-color: ${(props) =>
props.theme.cardBackground};
border: 1px solid ${(props) => props.theme.borderColor};
border-radius: 8px;
padding: 24px;
box-shadow: ${(props) => props.theme.elevation.low};
&:hover {
box-shadow: ${(props) => props.theme.elevation.medium};
}
`;
// テーマ定義
const lightTheme = {
backgroundColor: '#ffffff',
textColor: '#333333',
cardBackground: '#f8f9fa',
borderColor: '#dee2e6',
elevation: {
low: '0 1px 3px rgba(0,0,0,0.12)',
medium: '0 4px 6px rgba(0,0,0,0.16)',
},
};
const darkTheme = {
backgroundColor: '#1a1a1a',
textColor: '#ffffff',
cardBackground: '#2d2d2d',
borderColor: '#404040',
elevation: {
low: '0 1px 3px rgba(255,255,255,0.12)',
medium: '0 4px 6px rgba(255,255,255,0.16)',
},
};
typescript// Emotion版
import { css, Global, ThemeProvider } from '@emotion/react';
import styled from '@emotion/styled';
const globalStyles = (theme) => css`
body {
background-color: ${theme.backgroundColor};
color: ${theme.textColor};
transition: all 0.3s ease;
}
`;
const Card = styled.div`
background-color: ${(props) =>
props.theme.cardBackground};
border: 1px solid ${(props) => props.theme.borderColor};
border-radius: 8px;
padding: 24px;
box-shadow: ${(props) => props.theme.elevation.low};
&:hover {
box-shadow: ${(props) => props.theme.elevation.medium};
}
`;
// アプリケーションルート
const App = () => {
const [isDark, setIsDark] = useState(false);
const theme = isDark ? darkTheme : lightTheme;
return (
<ThemeProvider theme={theme}>
<Global styles={globalStyles} />
<Card>
<h2>テーマ対応カード</h2>
<button onClick={() => setIsDark(!isDark)}>
{isDark ? 'ライトモード' : 'ダークモード'}
に切り替え
</button>
</Card>
</ThemeProvider>
);
};
アニメーション実装の比較
動的な UI に欠かせないアニメーション実装も比較してみましょう。
styled-components でのアニメーション:
typescriptimport styled, { keyframes, css } from 'styled-components';
const slideIn = keyframes`
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
`;
const pulse = keyframes`
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
`;
const AnimatedButton = styled.button`
animation: ${slideIn} 0.5s ease-out;
${(props) =>
props.loading &&
css`
animation: ${pulse} 1s infinite;
pointer-events: none;
`}
&:hover {
transform: translateY(-2px);
transition: transform 0.2s ease;
}
`;
Emotion でのアニメーション:
typescriptimport { css, keyframes } from '@emotion/react';
import styled from '@emotion/styled';
const slideIn = keyframes`
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
`;
const loadingAnimation = css`
animation: ${keyframes`
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
`} 1s infinite;
pointer-events: none;
`;
const AnimatedButton = styled.button`
animation: ${slideIn} 0.5s ease-out;
${(props) => props.loading && loadingAnimation}
&:hover {
transform: translateY(-2px);
transition: transform 0.2s ease;
}
`;
パフォーマンス計測の実装例
実際のアプリケーションでパフォーマンスを計測するコードもご紹介します。
typescriptimport { Profiler } from 'react';
const PerformanceTest = () => {
const onRenderCallback = (id, phase, actualDuration) => {
console.log(`${id} ${phase}: ${actualDuration}ms`);
};
return (
<Profiler
id='StyledComponents'
onRender={onRenderCallback}
>
{/* 大量のスタイル付きコンポーネント */}
{Array.from({ length: 1000 }, (_, i) => (
<StyledButton
key={i}
variant={i % 2 === 0 ? 'primary' : 'secondary'}
>
Button {i}
</StyledButton>
))}
</Profiler>
);
};
まとめ - どちらを選ぶべきか
この記事を通じて、styled-components と Emotion の詳細な比較をしてまいりました。最終的にどちらを選ぶべきかは、あなたのプロジェクトの特性と価値観によって決まります。
styled-components を選ぶべき場面
以下の条件に当てはまる場合は、styled-components をお勧めします:
# | 条件 | 理由 |
---|---|---|
1 | チーム開発が主体 | 豊富なドキュメントと学習リソース |
2 | 安定性を重視 | 長期間の実績とコミュニティサポート |
3 | CSS-in-JS 初心者 | 直感的な記法で学習コストが低い |
4 | 既存プロジェクトでの採用 | エコシステムの充実と移行コスト |
Emotion を選ぶべき場面
一方、以下の場合は Emotion の方が適しているでしょう:
# | 条件 | 理由 |
---|---|---|
1 | パフォーマンス重視 | 軽量なバンドルサイズと高速なランタイム |
2 | 柔軟な記法が必要 | css 記法と styled 記法の使い分け |
3 | モダンな開発環境 | 最新の最適化技術とツールチェーン |
4 | 新規プロジェクト | 制約なく最適な選択が可能 |
心に留めておいていただきたいこと
技術選択において最も大切なのは、チーム全体の生産性と開発体験の向上です。ライブラリの性能差よりも、チームメンバーが快適に開発できる環境を構築することが、長期的なプロジェクトの成功につながります。
どちらを選んだとしても、以下の原則を忘れずにいてください:
- 一貫性のあるコーディングスタイルの確立
- 適切なコンポーネント設計による保守性の向上
- パフォーマンス計測による継続的な改善
- チーム全体でのナレッジ共有
あなたのプロジェクトが、ユーザーにとって価値のあるプロダクトとして成長していくことを心から願っています。styled-components と Emotion は、どちらもその目標達成のための強力なツールになってくれるはずです。
関連リンク
公式ドキュメント
TypeScript 関連
パフォーマンス・ベンチマーク
エコシステム
コミュニティ・学習リソース
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来