React+Emotionでスタイリング|CSS in JSの基本から実践まで
React+Emotionを使ったCSS in JSの基本から実践的な活用方法まで解説。スタイリングの効率化とメンテナンス性の向上を図れます。
みなさん、Reactでスタイリングするときに、CSSファイルの管理で困ったことはありませんか?
「コンポーネントが増えるとCSSがぐちゃぐちゃになる」「スタイルの競合が起きてしまう」「動的なスタイル変更が難しい」といった悩みを抱えている方も多いでしょう。
この記事では、そんな悩みを解決してくれるEmotionというCSS in JSライブラリをご紹介します。 EmotionとReactを組み合わせれば、スタイリングがもっと楽しく、効率的になりますよ。
Emotionとは何か?
Emotionは、JavaScriptの中でCSSを記述できるCSS in JSライブラリです。 従来のCSSファイルとは異なり、コンポーネントと密に連携したスタイリングが可能になります。
Emotionが選ばれる理由
Emotionが多くのプロジェクトで選ばれる理由は以下の通りです。
コンポーネントスコープ スタイルがコンポーネントに限定されるため、クラス名の衝突を避けられます。
動的スタイリング propsに応じた動的なスタイル変更が簡単に実装できます。
高いパフォーマンス 効率的なCSS生成とキャッシュ機能により、パフォーマンスが向上します。
優れた開発者体験 型安全性とIDEサポートにより、開発効率が向上します。
簡単に言うと、CSS管理の煩雑さを解決しながら、Reactの開発効率を向上させるツールです。
Emotionのセットアップをしよう
まず、Emotionをプロジェクトに導入する方法を見てみましょう。
インストール
Emotionの必要パッケージをインストールします。
# npm の場合
npm install @emotion/react @emotion/styled
# yarn の場合
yarn add @emotion/react @emotion/styled
Babel設定(オプション)
より効率的な開発のため、Babel設定を追加することもできます。
{
"presets": [
[
"@babel/preset-react",
{ "runtime": "automatic", "importSource": "@emotion/react" }
]
],
"plugins": ["@emotion/babel-plugin"]
}
TypeScript設定
TypeScriptを使用している場合は、tsconfig.jsonを設定します。
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react"
}
}
これで、Emotionを使う準備が整いました。
基本的なスタイリング方法を覚えよう
Emotionの基本的な使い方を見てみましょう。
css propを使った基本的なスタイリング
最も基本的なスタイリング方法は、css propを使用することです。
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const buttonStyle = css`
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
&:hover {
background-color: #0056b3;
}
`;
function Button({ children }) {
return <button css={buttonStyle}>{children}</button>;
}
export default Button;
このように、CSS-in-JSの記法でスタイルを定義できます。
インラインスタイルでの記述
より動的なスタイリングには、インラインでcssを記述することもできます。
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
function Alert({ type, children }) {
return (
<div
css={css`
padding: 16px;
border-radius: 4px;
margin: 10px 0;
background-color: ${type === 'error' ? '#f8d7da' : '#d4edda'};
color: ${type === 'error' ? '#721c24' : '#155724'};
border: 1px solid ${type === 'error' ? '#f5c6cb' : '#c3e6cb'};
`}
>
{children}
</div>
);
}
export default Alert;
propsに応じて動的にスタイルを変更することができます。 とても便利ですね。
styled-componentsスタイルの記述を試してみよう
Emotionでは、styled-components風の記述も可能です。
基本的なstyled記法
styledを使った基本的なコンポーネント作成方法です。
import styled from '@emotion/styled';
const StyledButton = styled.button`
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
&:hover {
background-color: #0056b3;
}
&:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
`;
function App() {
return (
<div>
<StyledButton>通常のボタン</StyledButton>
<StyledButton disabled>無効なボタン</StyledButton>
</div>
);
}
export default App;
propsを使った動的スタイリング
propsを使用して、動的にスタイルを変更することができます。
import styled from '@emotion/styled';
const StyledButton = styled.button`
background-color: ${props =>
props.variant === 'primary' ? '#007bff' :
props.variant === 'secondary' ? '#6c757d' :
'#28a745'
};
color: white;
padding: ${props => props.size === 'large' ? '15px 30px' : '10px 20px'};
border: none;
border-radius: 4px;
cursor: pointer;
font-size: ${props => props.size === 'large' ? '18px' : '16px'};
&:hover {
opacity: 0.9;
}
`;
function App() {
return (
<div>
<StyledButton variant="primary">プライマリ</StyledButton>
<StyledButton variant="secondary" size="large">
セカンダリ(大)
</StyledButton>
<StyledButton variant="success">成功</StyledButton>
</div>
);
}
export default App;
このように、propsを使って柔軟にスタイルを制御できます。
テーマ機能を活用しよう
Emotionでは、テーマ機能を使って一貫したデザインシステムを構築できます。
テーマの定義
まず、アプリケーション全体で使用するテーマを定義します。
// theme.js
export const theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
light: '#f8f9fa',
dark: '#343a40',
white: '#ffffff',
gray: {
100: '#f8f9fa',
200: '#e9ecef',
300: '#dee2e6',
400: '#ced4da',
500: '#adb5bd',
600: '#6c757d',
700: '#495057',
800: '#343a40',
900: '#212529'
}
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px'
},
breakpoints: {
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px'
},
typography: {
fontFamily: '"Helvetica Neue", Arial, sans-serif',
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
md: '1rem',
lg: '1.125rem',
xl: '1.25rem',
xxl: '1.5rem'
},
fontWeight: {
light: 300,
normal: 400,
medium: 500,
semibold: 600,
bold: 700
}
}
};
テーマプロバイダーの設定
アプリケーション全体でテーマを使用するための設定です。
// App.js
import { ThemeProvider } from '@emotion/react';
import { theme } from './theme';
import MainComponent from './MainComponent';
function App() {
return (
<ThemeProvider theme={theme}>
<MainComponent />
</ThemeProvider>
);
}
export default App;
テーマを使ったスタイリング
定義したテーマを使ってスタイリングを行います。
import styled from '@emotion/styled';
const Container = styled.div`
max-width: 1200px;
margin: 0 auto;
padding: ${props => props.theme.spacing.md};
@media (max-width: ${props => props.theme.breakpoints.md}) {
padding: ${props => props.theme.spacing.sm};
}
`;
const Title = styled.h1`
font-family: ${props => props.theme.typography.fontFamily};
font-size: ${props => props.theme.typography.fontSize.xxl};
font-weight: ${props => props.theme.typography.fontWeight.bold};
color: ${props => props.theme.colors.dark};
margin-bottom: ${props => props.theme.spacing.lg};
`;
const Card = styled.div`
background-color: ${props => props.theme.colors.white};
border: 1px solid ${props => props.theme.colors.gray[200]};
border-radius: 8px;
padding: ${props => props.theme.spacing.lg};
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`;
function MainComponent() {
return (
<Container>
<Title>Emotionテーマ活用例</Title>
<Card>
<p>テーマを使って一貫したデザインを実現できます。</p>
</Card>
</Container>
);
}
export default MainComponent;
このように、テーマを使うことで一貫したデザインシステムを構築できます。
レスポンシブデザインを実装しよう
Emotionでレスポンシブデザインを実装する方法を見てみましょう。
メディアクエリの活用
Emotionでは、CSS-in-JSの中でメディアクエリを使用できます。
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const responsiveContainer = css`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
padding: 10px;
}
@media (max-width: 480px) {
gap: 10px;
}
`;
const responsiveCard = css`
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@media (max-width: 768px) {
padding: 15px;
}
@media (max-width: 480px) {
padding: 10px;
}
`;
function ResponsiveGrid({ items }) {
return (
<div css={responsiveContainer}>
{items.map((item, index) => (
<div key={index} css={responsiveCard}>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
))}
</div>
);
}
export default ResponsiveGrid;
テーマを使ったレスポンシブ設計
テーマのブレイクポイントを活用したレスポンシブ設計です。
import styled from '@emotion/styled';
const ResponsiveLayout = styled.div`
display: flex;
flex-wrap: wrap;
gap: ${props => props.theme.spacing.md};
@media (max-width: ${props => props.theme.breakpoints.md}) {
flex-direction: column;
gap: ${props => props.theme.spacing.sm};
}
`;
const FlexItem = styled.div`
flex: 1;
min-width: 250px;
padding: ${props => props.theme.spacing.lg};
background: ${props => props.theme.colors.gray[100]};
border-radius: 8px;
@media (max-width: ${props => props.theme.breakpoints.sm}) {
min-width: 100%;
padding: ${props => props.theme.spacing.md};
}
`;
function ResponsiveComponent() {
return (
<ResponsiveLayout>
<FlexItem>
<h3>コンテンツ1</h3>
<p>レスポンシブなレイアウトのデモです。</p>
</FlexItem>
<FlexItem>
<h3>コンテンツ2</h3>
<p>画面サイズに応じて表示が変わります。</p>
</FlexItem>
</ResponsiveLayout>
);
}
export default ResponsiveComponent;
テーマを使うことで、一貫したレスポンシブデザインを実現できます。
アニメーションとトランジションを追加しよう
Emotionでアニメーションを実装する方法を見てみましょう。
CSS トランジションの実装
基本的なホバーアニメーションの実装例です。
import styled from '@emotion/styled';
const AnimatedButton = styled.button`
background: linear-gradient(45deg, #007bff, #0056b3);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
transform: translateY(0);
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.4);
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(0, 123, 255, 0.6);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(0, 123, 255, 0.3);
}
`;
const FadeInCard = styled.div`
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease forwards;
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
`;
function AnimatedComponents() {
return (
<div>
<AnimatedButton>ホバーしてみてください</AnimatedButton>
<FadeInCard>
<h3>フェードインアニメーション</h3>
<p>カードが下から上にフェードインします。</p>
</FadeInCard>
</div>
);
}
export default AnimatedComponents;
keyframesを使った高度なアニメーション
より複雑なアニメーションの実装例です。
/** @jsxImportSource @emotion/react */
import { css, keyframes } from '@emotion/react';
const pulseAnimation = keyframes`
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
`;
const rotateAnimation = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const pulseStyle = css`
animation: ${pulseAnimation} 2s ease-in-out infinite;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
margin: 20px 0;
`;
const spinnerStyle = css`
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: ${rotateAnimation} 1s linear infinite;
margin: 20px auto;
`;
function AnimatedDemo() {
return (
<div>
<div css={pulseStyle}>
<h3>パルスアニメーション</h3>
<p>継続的に拡大縮小するアニメーション</p>
</div>
<div css={spinnerStyle}></div>
</div>
);
}
export default AnimatedDemo;
このように、Emotionを使って様々なアニメーション効果を実装できます。
パフォーマンスを最適化しよう
Emotionを使用する際のパフォーマンス最適化について解説します。
スタイルの分割と再利用
コンポーネント間でスタイルを効率的に再利用する方法です。
// styles/common.js
import { css } from '@emotion/react';
export const commonStyles = {
button: css`
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
`,
card: css`
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
`,
flexCenter: css`
display: flex;
justify-content: center;
align-items: center;
`
};
// 使用例
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import { commonStyles } from './styles/common';
const primaryButton = css`
${commonStyles.button}
background-color: #007bff;
color: white;
&:hover {
background-color: #0056b3;
}
`;
function MyComponent() {
return (
<div css={commonStyles.card}>
<div css={commonStyles.flexCenter}>
<button css={primaryButton}>クリック</button>
</div>
</div>
);
}
export default MyComponent;
メモ化による最適化
React.memoを使ってスタイルの再計算を最適化する方法です。
import React, { memo } from 'react';
import styled from '@emotion/styled';
const StyledCard = styled.div`
background: ${props => props.theme.colors.white};
border-radius: 8px;
padding: ${props => props.theme.spacing.lg};
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-left: 4px solid ${props =>
props.status === 'success' ? props.theme.colors.success :
props.status === 'error' ? props.theme.colors.danger :
props.theme.colors.info
};
`;
const Card = memo(({ title, content, status }) => {
return (
<StyledCard status={status}>
<h3>{title}</h3>
<p>{content}</p>
</StyledCard>
);
});
export default Card;
条件付きスタイリングの最適化
条件に応じたスタイリングを効率的に行う方法です。
import styled from '@emotion/styled';
const OptimizedButton = styled.button`
${props => props.theme.commonStyles.button}
${props => {
switch (props.variant) {
case 'primary':
return `
background-color: ${props.theme.colors.primary};
color: white;
&:hover { background-color: ${props.theme.colors.primaryDark}; }
`;
case 'secondary':
return `
background-color: ${props.theme.colors.secondary};
color: white;
&:hover { background-color: ${props.theme.colors.secondaryDark}; }
`;
default:
return `
background-color: transparent;
color: ${props.theme.colors.primary};
border: 1px solid ${props.theme.colors.primary};
&:hover { background-color: ${props.theme.colors.primaryLight}; }
`;
}
}}
`;
function OptimizedComponent() {
return (
<div>
<OptimizedButton variant="primary">プライマリ</OptimizedButton>
<OptimizedButton variant="secondary">セカンダリ</OptimizedButton>
<OptimizedButton>デフォルト</OptimizedButton>
</div>
);
}
export default OptimizedComponent;
これらの最適化により、パフォーマンスの向上を図ることができます。
実践的なコンポーネント例を作ってみよう
実際のプロジェクトで使える実践的なコンポーネント例を見てみましょう。
多機能なボタンコンポーネント
様々な用途に対応できるボタンコンポーネントです。
import styled from '@emotion/styled';
const StyledButton = styled.button`
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: ${props =>
props.size === 'small' ? '6px 12px' :
props.size === 'large' ? '16px 32px' :
'10px 20px'
};
font-size: ${props =>
props.size === 'small' ? '14px' :
props.size === 'large' ? '18px' :
'16px'
};
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
${props => {
const { variant, theme } = props;
const colors = theme.colors;
switch (variant) {
case 'primary':
return `
background: ${colors.primary};
color: white;
&:hover { background: ${colors.primaryDark}; }
`;
case 'secondary':
return `
background: ${colors.secondary};
color: white;
&:hover { background: ${colors.secondaryDark}; }
`;
case 'outline':
return `
background: transparent;
color: ${colors.primary};
border: 2px solid ${colors.primary};
&:hover {
background: ${colors.primary};
color: white;
}
`;
default:
return `
background: ${colors.gray[100]};
color: ${colors.dark};
&:hover { background: ${colors.gray[200]}; }
`;
}
}}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px ${props => props.theme.colors.primary}33;
}
`;
const LoadingSpinner = styled.div`
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
@keyframes spin {
to { transform: rotate(360deg); }
}
`;
function Button({
children,
variant = 'default',
size = 'medium',
loading = false,
disabled = false,
icon,
...props
}) {
return (
<StyledButton
variant={variant}
size={size}
disabled={disabled || loading}
{...props}
>
{loading && <LoadingSpinner />}
{!loading && icon && <span>{icon}</span>}
{children}
</StyledButton>
);
}
export default Button;
カスタマイズ可能なカードコンポーネント
柔軟性の高いカードコンポーネントの実装例です。
import styled from '@emotion/styled';
const CardContainer = styled.div`
background: ${props => props.theme.colors.white};
border-radius: 12px;
box-shadow: ${props =>
props.elevated ?
'0 10px 25px rgba(0, 0, 0, 0.1)' :
'0 2px 8px rgba(0, 0, 0, 0.08)'
};
overflow: hidden;
transition: all 0.3s ease;
border: 1px solid ${props => props.theme.colors.gray[200]};
${props => props.hoverable && `
&:hover {
transform: translateY(-2px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
}
`}
`;
const CardHeader = styled.div`
padding: ${props => props.theme.spacing.lg};
border-bottom: 1px solid ${props => props.theme.colors.gray[200]};
background: ${props => props.theme.colors.gray[50]};
`;
const CardTitle = styled.h3`
margin: 0;
color: ${props => props.theme.colors.dark};
font-size: ${props => props.theme.typography.fontSize.lg};
font-weight: ${props => props.theme.typography.fontWeight.semibold};
`;
const CardSubtitle = styled.p`
margin: 4px 0 0 0;
color: ${props => props.theme.colors.gray[600]};
font-size: ${props => props.theme.typography.fontSize.sm};
`;
const CardBody = styled.div`
padding: ${props => props.theme.spacing.lg};
`;
const CardFooter = styled.div`
padding: ${props => props.theme.spacing.md} ${props => props.theme.spacing.lg};
border-top: 1px solid ${props => props.theme.colors.gray[200]};
background: ${props => props.theme.colors.gray[50]};
display: flex;
justify-content: space-between;
align-items: center;
`;
function Card({
title,
subtitle,
children,
footer,
elevated = false,
hoverable = false,
...props
}) {
return (
<CardContainer elevated={elevated} hoverable={hoverable} {...props}>
{(title || subtitle) && (
<CardHeader>
{title && <CardTitle>{title}</CardTitle>}
{subtitle && <CardSubtitle>{subtitle}</CardSubtitle>}
</CardHeader>
)}
<CardBody>{children}</CardBody>
{footer && <CardFooter>{footer}</CardFooter>}
</CardContainer>
);
}
export default Card;
これらのコンポーネントは、実際のプロジェクトでそのまま使用できる実用的な例です。
よくある問題と解決方法
Emotionを使用する際によく遭遇する問題と解決方法をまとめました。
スタイルの競合問題
問題: 複数のスタイルが競合してしまう
解決方法:
// スタイルの優先順位を明確にする
const baseStyle = css`
color: blue;
`;
const overrideStyle = css`
${baseStyle}
color: red; /* これが優先される */
`;
// または、より具体的なセレクタを使用
const specificStyle = css`
&.my-component {
color: red;
}
`;
TypeScript との型安全性
問題: TypeScriptで型エラーが発生する
解決方法:
// 型定義を追加
interface ButtonProps {
variant: 'primary' | 'secondary' | 'outline';
size: 'small' | 'medium' | 'large';
children: React.ReactNode;
}
const StyledButton = styled.button<ButtonProps>`
padding: ${props => props.size === 'large' ? '16px 32px' : '10px 20px'};
background: ${props => props.variant === 'primary' ? '#007bff' : '#6c757d'};
`;
パフォーマンスの問題
問題: 大量のスタイルによるパフォーマンス低下
解決方法:
// スタイルの分割と再利用
const commonStyles = {
button: css`
/* 共通スタイル */
`,
card: css`
/* 共通スタイル */
`
};
// メモ化の活用
const MemoizedComponent = memo(styled.div`
/* スタイル */
`);
これらの対策により、安定したEmotion活用が可能になります。
まとめ:Emotionで効率的なスタイリングを実現しよう
React+Emotionを使ったCSS-in-JSスタイリングについて、基本から実践まで詳しく解説しました。
Emotionの主なメリット
コンポーネント指向 スタイルとコンポーネントの密結合により、保守性が向上します。
動的スタイリング propsに応じた柔軟なスタイル変更が可能です。
優れた開発者体験 型安全性とツールサポートにより、開発効率が向上します。
高いパフォーマンス 効率的なCSS生成とキャッシュ機能により、パフォーマンスが向上します。
豊富な機能 テーマ、アニメーション、レスポンシブ対応など、様々な機能を活用できます。
実装のポイント
効果的なEmotion活用のための重要なポイントです。
- 基本的な使い方の理解: css prop と styled の使い分け
- テーマシステムの活用: 一貫したデザインシステムの構築
- パフォーマンスの最適化: メモ化と効率的なスタイル管理
- 実践的なコンポーネント設計: 再利用可能で保守性の高いコンポーネント
学習の進め方
Emotionの習得は以下の順序で進めることをおすすめします。
- 基本的なスタイリング: css prop を使った基本的な記述
- styled-components: styled を使ったコンポーネント作成
- テーマ機能: 一貫したデザインシステムの構築
- レスポンシブ対応: メディアクエリとブレイクポイント
- アニメーション: トランジションとkeyframesの活用
- 最適化: パフォーマンスとメンテナンス性の向上
Emotionは、Reactアプリケーションのスタイリングを劇的に改善する強力なツールです。 コンポーネント指向の開発と相性が良く、保守性の高いスタイリングを実現できます。
ぜひ、実際のプロジェクトでEmotionを活用して、効率的で美しいUIを構築してみてください。