Emotion の css プロップで爆速スタイリング

React 開発者の皆さん、スタイリングで時間を取られすぎていませんか?
コンポーネント一つ一つに CSS ファイルを作成し、クラス名を考え、ファイルを行き来しながらスタイルを調整する。そんな煩雑な作業から解放されたいと思ったことはありませんか。
今日ご紹介するのは、Emotion のcss
プロップを使った革命的なスタイリング手法です。この手法を身につければ、驚くほどスムーズにスタイリングができるようになり、開発効率が格段に向上します。実際に私自身も、この手法を導入してからスタイリングにかかる時間が半分以下になりました。
この記事では、基本的な使い方から実践的な応用例まで、段階的に学習できるように構成しています。最後まで読んでいただければ、きっと「もっと早く知りたかった!」と思っていただけるはずです。
背景
従来のスタイリング手法の課題
Web 開発の歴史を振り返ると、スタイリング手法は常に進化し続けてきました。しかし、従来の手法には多くの課題がありました。
通常の CSS ファイル管理の問題点
typescript// 従来の方法:別々のファイルを管理
// Button.css
.button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
}
.button-primary {
background-color: #28a745;
}
.button-secondary {
background-color: #6c757d;
}
typescript// Button.tsx
import React from 'react';
import './Button.css';
const Button = ({ variant = 'primary', children }) => {
return (
<button className={`button button-${variant}`}>
{children}
</button>
);
};
この方法では、以下のような問題が発生します:
# | 問題点 | 具体的な影響 |
---|---|---|
1 | ファイル管理が複雑 | コンポーネントと CSS ファイルを別々に管理する必要がある |
2 | グローバルスコープの汚染 | クラス名が他のコンポーネントと衝突する可能性 |
3 | デッドコードの発生 | 使われなくなったスタイルが残りやすい |
4 | 動的スタイリングの困難さ | props に基づいたスタイル変更が複雑 |
CSS-in-JS の台頭と Emotion の位置づけ
これらの課題を解決するため、CSS-in-JS という概念が登場しました。JavaScript 内でスタイルを記述することで、コンポーネントとスタイルを密結合させ、より保守性の高いコードを書けるようになります。
CSS-in-JS ライブラリの比較
ライブラリ | 特徴 | 学習コスト | パフォーマンス |
---|---|---|---|
styled-components | テンプレートリテラル記法 | 中 | 良好 |
Emotion | 柔軟な API、軽量 | 低 | 優秀 |
JSS | 設定が豊富 | 高 | 良好 |
Emotion は、その中でも特に開発者体験(DX)を重視して設計されており、直感的で使いやすい API を提供しています。
課題
外部 CSS ファイルの管理コスト
大規模な React アプリケーションでは、CSS ファイルの管理が複雑になります。
typescript// 実際にプロジェクトで発生する管理コスト
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.css
│ │ └── Button.test.tsx
│ ├── Card/
│ │ ├── Card.tsx
│ │ ├── Card.css
│ │ └── Card.test.tsx
│ └── Modal/
│ ├── Modal.tsx
│ ├── Modal.css
│ └── Modal.test.tsx
└── styles/
├── globals.css
├── variables.css
└── reset.css
このファイル構成では、以下のような問題が発生します:
- ファイル数の増加: コンポーネントが増えるたびに CSS ファイルも増える
- インポート管理: 各コンポーネントで CSS ファイルのインポートが必要
- 依存関係の複雑化: どの CSS ファイルがどのコンポーネントで使われているか分からない
クラス名の衝突問題
グローバルスコープでのクラス名管理は、大きな問題を引き起こします。
css/* Header.css */
.container {
max-width: 1200px;
margin: 0 auto;
}
/* Footer.css */
.container {
max-width: 800px; /* 意図しない上書き */
margin: 0 auto;
}
このような衝突は、以下の問題を引き起こします:
- 予期しないスタイルの変更: 他のコンポーネントのスタイルが影響を受ける
- デバッグの困難さ: どの CSS ファイルが原因かを特定するのが困難
- メンテナンス性の低下: 安全にスタイルを変更することが困難
動的スタイリングの複雑さ
props や state に基づいてスタイルを動的に変更する場合、従来の方法では非常に複雑になります。
typescript// 従来の方法:複雑な条件分岐
const Button = ({ variant, size, disabled, loading }) => {
const getClassName = () => {
let className = 'button';
if (variant === 'primary')
className += ' button-primary';
if (variant === 'secondary')
className += ' button-secondary';
if (size === 'large') className += ' button-large';
if (size === 'small') className += ' button-small';
if (disabled) className += ' button-disabled';
if (loading) className += ' button-loading';
return className;
};
return (
<button className={getClassName()}>
{loading ? 'Loading...' : children}
</button>
);
};
この方法では、以下のような問題があります:
- コードの可読性低下: 条件分岐が多くなり、理解が困難
- 型安全性の欠如: TypeScript でもクラス名の型チェックができない
- リアルタイム更新の困難: 値の変更に応じたスタイル更新が複雑
解決策
css プロップの基本概念と仕組み
Emotion のcss
プロップは、これらの課題を根本から解決します。JavaScript のオブジェクト記法やテンプレートリテラルを使って、直接コンポーネントにスタイルを適用できます。
typescript// Emotionの基本的な使い方
import { css } from '@emotion/react';
const Button = ({ children }) => {
return (
<button
css={css`
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #0056b3;
}
`}
>
{children}
</button>
);
};
この方法の革新的な点は、以下の通りです:
- コンポーネントと一体化: スタイルがコンポーネントと同じファイルに記述される
- スコープの自動化: 自動的にユニークなクラス名が生成される
- JavaScript 連携: props や state を直接スタイルに反映できる
Emotion が提供する開発体験
Emotion は、開発者にとって理想的な開発体験を提供します。
開発効率の向上
typescript// ファイル間の移動が不要
const Card = ({ elevated, children }) => (
<div
css={css`
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: ${elevated
? '0 4px 8px rgba(0,0,0,0.1)'
: 'none'};
transition: box-shadow 0.2s ease;
`}
>
{children}
</div>
);
型安全性の確保
typescript// TypeScriptとの完璧な統合
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
variant,
size,
children,
}) => {
const getVariantStyles = () => {
switch (variant) {
case 'primary':
return '#007bff';
case 'secondary':
return '#6c757d';
case 'danger':
return '#dc3545';
}
};
const getSizeStyles = () => {
switch (size) {
case 'small':
return { padding: '4px 8px', fontSize: '12px' };
case 'medium':
return { padding: '8px 16px', fontSize: '14px' };
case 'large':
return { padding: '12px 24px', fontSize: '16px' };
}
};
return (
<button
css={css`
background-color: ${getVariantStyles()};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
${getSizeStyles()}
&:hover {
opacity: 0.9;
}
`}
>
{children}
</button>
);
};
具体例
css プロップの基本的な使い方
まず、Emotion の基本的なセットアップから始めましょう。
bash# Emotionのインストール
yarn add @emotion/react @emotion/styled
# TypeScriptを使用している場合
yarn add -D @emotion/babel-plugin
Next.js プロジェクトでの設定:
typescript// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
emotion: true,
},
};
module.exports = nextConfig;
基本的な使い方パターン
typescriptimport { css } from '@emotion/react';
// パターン1: テンプレートリテラル
const Container = () => (
<div
css={css`
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
`}
>
<h1>Welcome to Emotion!</h1>
</div>
);
typescript// パターン2: オブジェクト記法
const Container = () => (
<div
css={{
maxWidth: 1200,
margin: '0 auto',
padding: '0 16px',
backgroundColor: '#f5f5f5',
}}
>
<h1>Welcome to Emotion!</h1>
</div>
);
よくあるエラーと解決方法
typescript// ❌ よくあるエラー
TypeError: Cannot read properties of undefined (reading 'css')
// 解決方法: JSXプラグマの追加
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
動的スタイリングの実装
props に基づいた動的スタイリングが、Emotion の真の力を発揮する場面です。
typescript// 動的スタイリングの実装例
interface AlertProps {
type: 'success' | 'warning' | 'error' | 'info';
children: React.ReactNode;
dismissible?: boolean;
}
const Alert: React.FC<AlertProps> = ({
type,
children,
dismissible = false,
}) => {
const getAlertStyles = () => {
const baseStyles = {
padding: '12px 16px',
borderRadius: '4px',
marginBottom: '16px',
border: '1px solid',
position: 'relative' as const,
};
const typeStyles = {
success: {
backgroundColor: '#d4edda',
borderColor: '#c3e6cb',
color: '#155724',
},
warning: {
backgroundColor: '#fff3cd',
borderColor: '#ffeaa7',
color: '#856404',
},
error: {
backgroundColor: '#f8d7da',
borderColor: '#f5c6cb',
color: '#721c24',
},
info: {
backgroundColor: '#cce7ff',
borderColor: '#bee5eb',
color: '#0c5460',
},
};
return { ...baseStyles, ...typeStyles[type] };
};
return (
<div css={getAlertStyles()}>
{children}
{dismissible && (
<button
css={css`
position: absolute;
right: 8px;
top: 8px;
background: none;
border: none;
font-size: 16px;
cursor: pointer;
opacity: 0.7;
&:hover {
opacity: 1;
}
`}
>
×
</button>
)}
</div>
);
};
使用例
typescriptconst App = () => (
<div>
<Alert type='success' dismissible>
データが正常に保存されました!
</Alert>
<Alert type='warning'>
このアクションは取り消せません。
</Alert>
<Alert type='error'>
エラーが発生しました。再試行してください。
</Alert>
</div>
);
TypeScript との組み合わせ
TypeScript と Emotion を組み合わせることで、型安全なスタイリングが可能になります。
typescript// テーマの型定義
interface Theme {
colors: {
primary: string;
secondary: string;
success: string;
warning: string;
error: string;
};
spacing: {
xs: number;
sm: number;
md: number;
lg: number;
xl: number;
};
breakpoints: {
mobile: string;
tablet: string;
desktop: string;
};
}
// テーマオブジェクト
const theme: Theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
warning: '#ffc107',
error: '#dc3545',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
breakpoints: {
mobile: '@media (max-width: 768px)',
tablet: '@media (max-width: 1024px)',
desktop: '@media (min-width: 1025px)',
},
};
typescript// ThemeProviderの設定
import { ThemeProvider } from '@emotion/react';
const App = () => (
<ThemeProvider theme={theme}>
<MainContent />
</ThemeProvider>
);
// テーマを使用したコンポーネント
const StyledButton = ({
children,
}: {
children: React.ReactNode;
}) => (
<button
css={(theme: Theme) => css`
background-color: ${theme.colors.primary};
padding: ${theme.spacing.sm}px ${theme.spacing.md}px;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
opacity: 0.9;
}
${theme.breakpoints.mobile} {
padding: ${theme.spacing.xs}px ${theme.spacing.sm}px;
font-size: 14px;
}
`}
>
{children}
</button>
);
型安全性のメリット
typescript// ❌ 存在しないプロパティでエラーが発生
const WrongComponent = () => (
<div
css={(theme: Theme) => css`
color: ${theme.colors
.nonexistent}; // TypeScriptエラー
`}
>
Content
</div>
);
// ✅ 正しい使用方法
const CorrectComponent = () => (
<div
css={(theme: Theme) => css`
color: ${theme.colors.primary}; // OK
`}
>
Content
</div>
);
レスポンシブデザインへの対応
Emotion を使用したレスポンシブデザインの実装は、非常に直感的です。
typescript// レスポンシブデザインの実装例
const ResponsiveCard = ({
children,
}: {
children: React.ReactNode;
}) => (
<div
css={css`
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/* デスクトップファースト */
@media (max-width: 1024px) {
padding: 16px;
}
@media (max-width: 768px) {
padding: 12px;
border-radius: 4px;
}
@media (max-width: 480px) {
padding: 8px;
border-radius: 0;
box-shadow: none;
border-bottom: 1px solid #eee;
}
`}
>
{children}
</div>
);
より高度なレスポンシブグリッドシステム
typescript// グリッドシステムの実装
interface GridProps {
columns?: number;
gap?: number;
children: React.ReactNode;
}
const Grid: React.FC<GridProps> = ({
columns = 3,
gap = 16,
children,
}) => (
<div
css={css`
display: grid;
grid-template-columns: repeat(${columns}, 1fr);
gap: ${gap}px;
@media (max-width: 1024px) {
grid-template-columns: repeat(
${Math.max(1, columns - 1)},
1fr
);
}
@media (max-width: 768px) {
grid-template-columns: repeat(
${Math.max(1, columns - 2)},
1fr
);
gap: ${gap * 0.75}px;
}
@media (max-width: 480px) {
grid-template-columns: 1fr;
gap: ${gap * 0.5}px;
}
`}
>
{children}
</div>
);
テーマ変数の活用
テーマシステムを活用することで、一貫性のあるデザインシステムを構築できます。
typescript// 包括的なテーマシステム
const designSystem = {
colors: {
// Primary colors
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
900: '#1e3a8a',
},
// Semantic colors
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
// Neutral colors
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
},
},
typography: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
},
fontWeight: {
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
},
},
spacing: {
px: '1px',
0: '0',
1: '0.25rem',
2: '0.5rem',
3: '0.75rem',
4: '1rem',
5: '1.25rem',
6: '1.5rem',
8: '2rem',
10: '2.5rem',
12: '3rem',
16: '4rem',
20: '5rem',
24: '6rem',
},
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
base: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
},
borderRadius: {
none: '0',
sm: '0.125rem',
base: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
xl: '0.75rem',
'2xl': '1rem',
full: '9999px',
},
};
テーマを活用したコンポーネント
typescript// テーマを活用したボタンコンポーネント
interface ButtonProps {
variant?:
| 'primary'
| 'secondary'
| 'success'
| 'warning'
| 'error';
size?: 'sm' | 'base' | 'lg';
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'base',
children,
onClick,
disabled = false,
}) => {
const getVariantStyles = () => {
switch (variant) {
case 'primary':
return {
backgroundColor: designSystem.colors.primary[500],
color: 'white',
'&:hover': !disabled
? {
backgroundColor:
designSystem.colors.primary[600],
}
: {},
};
case 'secondary':
return {
backgroundColor: designSystem.colors.gray[100],
color: designSystem.colors.gray[800],
'&:hover': !disabled
? {
backgroundColor:
designSystem.colors.gray[200],
}
: {},
};
case 'success':
return {
backgroundColor: designSystem.colors.success,
color: 'white',
'&:hover': !disabled
? {
backgroundColor: '#059669',
}
: {},
};
case 'warning':
return {
backgroundColor: designSystem.colors.warning,
color: 'white',
'&:hover': !disabled
? {
backgroundColor: '#d97706',
}
: {},
};
case 'error':
return {
backgroundColor: designSystem.colors.error,
color: 'white',
'&:hover': !disabled
? {
backgroundColor: '#dc2626',
}
: {},
};
default:
return {};
}
};
const getSizeStyles = () => {
switch (size) {
case 'sm':
return {
padding: `${designSystem.spacing[2]} ${designSystem.spacing[3]}`,
fontSize: designSystem.typography.fontSize.sm,
};
case 'base':
return {
padding: `${designSystem.spacing[3]} ${designSystem.spacing[4]}`,
fontSize: designSystem.typography.fontSize.base,
};
case 'lg':
return {
padding: `${designSystem.spacing[4]} ${designSystem.spacing[6]}`,
fontSize: designSystem.typography.fontSize.lg,
};
default:
return {};
}
};
return (
<button
css={css`
border: none;
border-radius: ${designSystem.borderRadius.md};
cursor: ${disabled ? 'not-allowed' : 'pointer'};
font-weight: ${designSystem.typography.fontWeight
.medium};
transition: all 0.2s ease;
opacity: ${disabled ? 0.6 : 1};
box-shadow: ${designSystem.shadows.sm};
&:focus {
outline: none;
box-shadow: ${designSystem.shadows.md};
}
${getVariantStyles()}
${getSizeStyles()}
`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
使用例とバリエーション
typescriptconst ButtonShowcase = () => (
<div
css={css`
display: flex;
gap: ${designSystem.spacing[4]};
flex-wrap: wrap;
padding: ${designSystem.spacing[8]};
`}
>
<Button variant='primary' size='sm'>
小さいボタン
</Button>
<Button variant='secondary'>標準ボタン</Button>
<Button variant='success' size='lg'>
大きいボタン
</Button>
<Button variant='warning'>警告ボタン</Button>
<Button variant='error' disabled>
無効ボタン
</Button>
</div>
);
まとめ
Emotion のcss
プロップを使ったスタイリング手法は、現代の React 開発において革命的な変化をもたらします。
得られる主なメリット
項目 | 従来の方法 | Emotion の css プロップ |
---|---|---|
開発効率 | CSS ファイルとの往復が必要 | コンポーネント内で完結 |
保守性 | グローバルスコープでの管理 | スコープが自動的に分離 |
型安全性 | クラス名の型チェック困難 | TypeScript で完全な型チェック |
動的スタイリング | 複雑な条件分岐が必要 | JavaScript 式で直接記述 |
パフォーマンス | 未使用 CSS の残存リスク | 必要なスタイルのみ生成 |
開発者として感じる変化
この手法を導入することで、以下のような変化を実感できるでしょう:
- 集中力の向上: ファイル間の移動が不要になり、コンポーネントの実装に集中できます
- バグの減少: スコープが自動的に分離されるため、スタイルの衝突が発生しません
- メンテナンス性の向上: コンポーネントとスタイルが一体化しているため、変更が容易です
- チーム開発の効率化: 一貫したスタイリング手法により、チーム全体の開発効率が向上します
今後の学習ステップ
Emotion を更に活用するために、以下の学習をお勧めします:
styled
コンポーネントの活用: より再利用性の高いコンポーネント設計- パフォーマンス最適化:
useMemo
との組み合わせによる最適化 - アニメーション連携:
framer-motion
などとの組み合わせ - テストアプローチ: Emotion スタイルのテスト手法
React 開発者として、効率的で保守性の高いスタイリングを身につけることは、あなたの技術力を大きく向上させるでしょう。今日から始めて、より良い開発体験を手に入れてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来