Emotion と React の親和性:実践コンポーネント設計術

React でスタイリングを行う際、多くの開発者が直面する課題があります。CSS Modules、Styled Components、そして今回取り上げる Emotion。数ある選択肢の中から、なぜ Emotion と React の組み合わせが特別なのか、実際の開発現場での体験を通してお伝えします。
Emotion は単なる CSS-in-JS ライブラリではありません。React のコンポーネント思想と完璧に調和し、開発者の創造性を最大限に引き出すツールなのです。この記事では、Emotion と React の親和性を深く理解し、実践的なコンポーネント設計の技術を身につけていただきます。
Emotion と React の相性が良い理由
React のコンポーネント思想との親和性
React の核となる思想は「コンポーネントベースの開発」です。UI を小さな部品に分割し、それぞれを独立して管理・再利用するという考え方ですね。Emotion はこの思想と完璧に調和します。
従来の CSS では、スタイルとコンポーネントが分離されていました。しかし、Emotion を使うことで、スタイルがコンポーネントの一部として自然に統合されるのです。
typescript// 従来の CSS アプローチ
// Button.css
.button {
background: blue;
color: white;
padding: 10px 20px;
}
// Button.tsx
import './Button.css';
const Button = ({ children }) => {
return <button className="button">{children}</button>;
};
Emotion を使った場合、スタイルがコンポーネントと一体になります:
typescript// Emotion を使ったアプローチ
import styled from '@emotion/styled';
const StyledButton = styled.button`
background: blue;
color: white;
padding: 10px 20px;
`;
const Button = ({ children }) => {
return <StyledButton>{children}</StyledButton>;
};
この違いは、コンポーネントの移動や削除時に顕著に現れます。CSS ファイルの依存関係を気にする必要がなく、コンポーネントとスタイルが常に一緒に管理されるのです。
JSX との自然な統合
Emotion の最大の魅力は、JSX との自然な統合にあります。JavaScript の式や変数を直接スタイルに埋め込むことができ、動的なスタイリングが直感的に実現できます。
typescriptimport styled from '@emotion/styled';
const DynamicButton = styled.button<{ isActive: boolean }>`
background: ${(props) =>
props.isActive ? 'green' : 'gray'};
color: white;
padding: 10px 20px;
transition: background 0.3s ease;
&:hover {
background: ${(props) =>
props.isActive ? 'darkgreen' : 'darkgray'};
}
`;
const App = () => {
const [isActive, setIsActive] = useState(false);
return (
<DynamicButton
isActive={isActive}
onClick={() => setIsActive(!isActive)}
>
クリックしてください
</DynamicButton>
);
};
このように、props を通じて動的にスタイルを変更できるのは、Emotion の強力な特徴です。React の状態管理とスタイルが自然に連携し、複雑な UI の実装が驚くほどシンプルになります。
動的スタイリングの実現
Emotion の真価は、複雑な動的スタイリングを実現する能力にあります。条件分岐、計算、アニメーションなど、従来の CSS では実現が困難だった機能を簡単に実装できます。
typescriptimport styled, { css } from '@emotion/styled';
const Card = styled.div<{
variant: 'primary' | 'secondary';
size: 'small' | 'medium' | 'large';
}>`
border-radius: 8px;
padding: ${(props) => {
switch (props.size) {
case 'small':
return '8px';
case 'medium':
return '16px';
case 'large':
return '24px';
default:
return '16px';
}
}};
${(props) =>
props.variant === 'primary' &&
css`
background: linear-gradient(
135deg,
#667eea 0%,
#764ba2 100%
);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
`}
${(props) =>
props.variant === 'secondary' &&
css`
background: #f8f9fa;
color: #333;
border: 1px solid #e9ecef;
`}
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
`;
このような動的スタイリングにより、コンポーネントの表現力が格段に向上します。デザインシステムの構築や、ユーザーインタラクションに応じた UI の変化を、コードレベルで完璧に制御できるようになるのです。
基本的なコンポーネント設計パターン
styled-components を使ったコンポーネント作成
Emotion の styled
関数を使うことで、HTML 要素や React コンポーネントを拡張したスタイル付きコンポーネントを作成できます。これが Emotion の最も基本的で強力な機能です。
まず、シンプルなボタンコンポーネントから始めてみましょう:
typescriptimport styled from '@emotion/styled';
// 基本的なボタンスタイル
const BaseButton = styled.button`
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;
// プライマリボタン
const PrimaryButton = styled(BaseButton)`
background: #007bff;
color: white;
padding: 12px 24px;
&:hover:not(:disabled) {
background: #0056b3;
transform: translateY(-1px);
}
`;
// セカンダリボタン
const SecondaryButton = styled(BaseButton)`
background: transparent;
color: #007bff;
border: 2px solid #007bff;
padding: 10px 22px;
&:hover:not(:disabled) {
background: #007bff;
color: white;
}
`;
このパターンの利点は、共通のスタイルを BaseButton
に定義し、それを継承して異なるバリエーションを作成できることです。DRY(Don't Repeat Yourself)の原則を守りながら、一貫性のあるデザインを実現できます。
props を使った動的スタイリング
Emotion の真髄は、props を通じて動的にスタイルを変更できることです。これにより、同じコンポーネントで様々な見た目を実現できます。
typescriptimport styled from '@emotion/styled';
// サイズとバリアントを props で制御するボタン
const Button = styled.button<{
size?: 'small' | 'medium' | 'large';
variant?: 'primary' | 'secondary' | 'danger';
fullWidth?: boolean;
}>`
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
width: ${(props) => (props.fullWidth ? '100%' : 'auto')};
// サイズによるスタイル変更
${(props) =>
props.size === 'small' &&
`
padding: 8px 16px;
font-size: 14px;
`}
${(props) =>
props.size === 'medium' &&
`
padding: 12px 24px;
font-size: 16px;
`}
${(props) =>
props.size === 'large' &&
`
padding: 16px 32px;
font-size: 18px;
`}
// バリアントによるスタイル変更
${(props) =>
props.variant === 'primary' &&
`
background: #007bff;
color: white;
&:hover:not(:disabled) {
background: #0056b3;
}
`}
${(props) =>
props.variant === 'secondary' &&
`
background: transparent;
color: #007bff;
border: 2px solid #007bff;
&:hover:not(:disabled) {
background: #007bff;
color: white;
}
`}
${(props) =>
props.variant === 'danger' &&
`
background: #dc3545;
color: white;
&:hover:not(:disabled) {
background: #c82333;
}
`}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;
// 使用例
const App = () => {
return (
<div>
<Button size='small' variant='primary'>
小さいボタン
</Button>
<Button size='medium' variant='secondary'>
中サイズボタン
</Button>
<Button size='large' variant='danger' fullWidth>
危険な操作
</Button>
</div>
);
};
このアプローチにより、コンポーネントの柔軟性が大幅に向上します。同じコンポーネントで様々な用途に対応でき、コードの重複を避けることができます。
コンポーネントの再利用性を高める設計
再利用可能なコンポーネントを設計する際の重要なポイントは、適切な抽象化とインターフェースの設計です。Emotion を使うことで、これらの設計がより直感的になります。
typescriptimport styled from '@emotion/styled';
import { forwardRef } from 'react';
// 共通のスタイル定義
const commonStyles = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s ease',
fontFamily: 'inherit',
textDecoration: 'none',
} as const;
// ベースコンポーネント
const BaseComponent = styled.div`
${commonStyles}
`;
// ボタンとして使用可能なコンポーネント
const ButtonComponent = styled.button`
${commonStyles}
`;
// リンクとして使用可能なコンポーネント
const LinkComponent = styled.a`
${commonStyles}
`;
// 再利用可能なアクションボタン
interface ActionButtonProps {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'small' | 'medium' | 'large';
as?: 'button' | 'a';
href?: string;
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
const ActionButton = forwardRef<
HTMLButtonElement | HTMLAnchorElement,
ActionButtonProps
>(
(
{
variant = 'primary',
size = 'medium',
as = 'button',
children,
...props
},
ref
) => {
const Component =
as === 'a' ? LinkComponent : ButtonComponent;
return (
<Component
ref={ref}
variant={variant}
size={size}
{...props}
>
{children}
</Component>
);
}
);
ActionButton.displayName = 'ActionButton';
この設計により、同じコンポーネントをボタンとしてもリンクとしても使用でき、様々なシナリオに対応できます。また、forwardRef
を使うことで、親コンポーネントから直接 DOM 要素にアクセスすることも可能になります。
高度なコンポーネント設計テクニック
テーマ機能を使った一貫性のあるデザイン
Emotion のテーマ機能は、アプリケーション全体で一貫したデザインを実現するための強力なツールです。色、フォント、スペーシングなどのデザイントークンを一元管理できます。
まず、テーマの型定義を作成します:
typescript// theme/types.ts
export interface Theme {
colors: {
primary: string;
secondary: string;
success: string;
danger: string;
warning: string;
info: string;
light: string;
dark: string;
white: string;
black: string;
};
typography: {
fontFamily: {
primary: string;
secondary: string;
};
fontSize: {
xs: string;
sm: string;
base: string;
lg: string;
xl: string;
'2xl': string;
};
fontWeight: {
normal: number;
medium: number;
semibold: number;
bold: number;
};
};
spacing: {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
'2xl': string;
};
borderRadius: {
sm: string;
md: string;
lg: string;
full: string;
};
shadows: {
sm: string;
md: string;
lg: string;
xl: string;
};
}
次に、実際のテーマオブジェクトを作成します:
typescript// theme/theme.ts
import { Theme } from './types';
export const lightTheme: Theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
light: '#f8f9fa',
dark: '#343a40',
white: '#ffffff',
black: '#000000',
},
typography: {
fontFamily: {
primary:
'"Inter", -apple-system, BlinkMacSystemFont, sans-serif',
secondary: '"Georgia", serif',
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
'2xl': '3rem',
},
borderRadius: {
sm: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
full: '9999px',
},
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
},
};
テーマを使用したコンポーネントの例:
typescriptimport styled from '@emotion/styled';
import { useTheme } from '@emotion/react';
const ThemedButton = styled.button`
background: ${(props) => props.theme.colors.primary};
color: ${(props) => props.theme.colors.white};
font-family: ${(props) =>
props.theme.typography.fontFamily.primary};
font-size: ${(props) =>
props.theme.typography.fontSize.base};
font-weight: ${(props) =>
props.theme.typography.fontWeight.medium};
padding: ${(props) =>
`${props.theme.spacing.sm} ${props.theme.spacing.md}`};
border-radius: ${(props) => props.theme.borderRadius.md};
border: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: ${(props) => props.theme.shadows.sm};
&:hover {
background: ${(props) => props.theme.colors.dark};
box-shadow: ${(props) => props.theme.shadows.md};
}
`;
// テーマプロバイダーの設定
import { ThemeProvider } from '@emotion/react';
const App = () => {
return (
<ThemeProvider theme={lightTheme}>
<ThemedButton>テーマ付きボタン</ThemedButton>
</ThemeProvider>
);
};
条件分岐を使ったスタイル切り替え
Emotion の css
関数と条件分岐を組み合わせることで、複雑なスタイルの切り替えを実現できます。
typescriptimport styled, { css } from '@emotion/styled';
interface CardProps {
variant: 'default' | 'elevated' | 'outlined';
size: 'small' | 'medium' | 'large';
interactive?: boolean;
}
const Card = styled.div<CardProps>`
background: ${(props) => props.theme.colors.white};
border-radius: ${(props) => props.theme.borderRadius.lg};
transition: all 0.3s ease;
// サイズによる条件分岐
${(props) => {
switch (props.size) {
case 'small':
return css`
padding: ${props.theme.spacing.sm};
font-size: ${props.theme.typography.fontSize.sm};
`;
case 'medium':
return css`
padding: ${props.theme.spacing.md};
font-size: ${props.theme.typography.fontSize
.base};
`;
case 'large':
return css`
padding: ${props.theme.spacing.lg};
font-size: ${props.theme.typography.fontSize.lg};
`;
default:
return css`
padding: ${props.theme.spacing.md};
font-size: ${props.theme.typography.fontSize
.base};
`;
}
}}
// バリアントによる条件分岐
${(props) =>
props.variant === 'default' &&
css`
border: 1px solid ${props.theme.colors.light};
box-shadow: ${props.theme.shadows.sm};
`}
${(props) =>
props.variant === 'elevated' &&
css`
border: none;
box-shadow: ${props.theme.shadows.lg};
`}
${(props) =>
props.variant === 'outlined' &&
css`
border: 2px solid ${props.theme.colors.primary};
box-shadow: none;
`}
// インタラクティブな要素の場合
${(props) =>
props.interactive &&
css`
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: ${props.theme.shadows.xl};
}
`}
`;
コンポーネントの合成と継承
Emotion では、既存のコンポーネントを基に新しいコンポーネントを作成できます。これにより、コードの再利用性と保守性が大幅に向上します。
typescriptimport styled from '@emotion/styled';
// ベースとなるカードコンポーネント
const BaseCard = styled.div`
background: ${(props) => props.theme.colors.white};
border-radius: ${(props) => props.theme.borderRadius.lg};
padding: ${(props) => props.theme.spacing.md};
box-shadow: ${(props) => props.theme.shadows.sm};
transition: all 0.3s ease;
`;
// 商品カード(BaseCard を継承)
const ProductCard = styled(BaseCard)`
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.spacing.sm};
&:hover {
transform: translateY(-4px);
box-shadow: ${(props) => props.theme.shadows.lg};
}
`;
// 商品画像
const ProductImage = styled.img`
width: 100%;
height: 200px;
object-fit: cover;
border-radius: ${(props) => props.theme.borderRadius.md};
`;
// 商品情報
const ProductInfo = styled.div`
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.spacing.xs};
`;
// 商品タイトル
const ProductTitle = styled.h3`
margin: 0;
font-size: ${(props) =>
props.theme.typography.fontSize.lg};
font-weight: ${(props) =>
props.theme.typography.fontWeight.semibold};
color: ${(props) => props.theme.colors.dark};
`;
// 商品価格
const ProductPrice = styled.span`
font-size: ${(props) =>
props.theme.typography.fontSize.xl};
font-weight: ${(props) =>
props.theme.typography.fontWeight.bold};
color: ${(props) => props.theme.colors.primary};
`;
// 使用例
const ProductCardComponent = ({ product }) => {
return (
<ProductCard>
<ProductImage
src={product.image}
alt={product.name}
/>
<ProductInfo>
<ProductTitle>{product.name}</ProductTitle>
<ProductPrice>
¥{product.price.toLocaleString()}
</ProductPrice>
</ProductInfo>
</ProductCard>
);
};
パフォーマンスを考慮した設計
不要な再レンダリングの回避
Emotion を使ったコンポーネントでも、React の再レンダリング最適化は重要です。特に動的なスタイルを持つコンポーネントでは、パフォーマンスの影響が大きくなります。
typescriptimport styled from '@emotion/styled';
import { memo, useMemo } from 'react';
// 動的スタイルを持つコンポーネント
const DynamicCard = styled.div<{
isActive: boolean;
color: string;
}>`
background: ${(props) => props.color};
opacity: ${(props) => (props.isActive ? 1 : 0.7)};
transform: ${(props) =>
props.isActive ? 'scale(1.05)' : 'scale(1)'};
transition: all 0.3s ease;
`;
// 最適化されていないコンポーネント(問題のある例)
const UnoptimizedCard = ({ isActive, color, children }) => {
return (
<DynamicCard isActive={isActive} color={color}>
{children}
</DynamicCard>
);
};
// 最適化されたコンポーネント
const OptimizedCard = memo(
({ isActive, color, children }) => {
// 動的スタイルをメモ化
const dynamicStyles = useMemo(
() => ({
backgroundColor: color,
opacity: isActive ? 1 : 0.7,
transform: isActive ? 'scale(1.05)' : 'scale(1)',
}),
[isActive, color]
);
return <div style={dynamicStyles}>{children}</div>;
}
);
OptimizedCard.displayName = 'OptimizedCard';
CSS-in-JS の最適化テクニック
Emotion のパフォーマンスを最大限に引き出すためのテクニックを紹介します。
typescriptimport styled, { css } from '@emotion/styled';
// 静的スタイルを事前定義
const staticStyles = css`
border-radius: 8px;
transition: all 0.3s ease;
cursor: pointer;
`;
// 動的スタイルを分離
const getDynamicStyles = (
variant: string,
size: string
) => css`
${variant === 'primary' &&
css`
background: #007bff;
color: white;
`}
${variant === 'secondary' &&
css`
background: #6c757d;
color: white;
`}
${size === 'small' &&
css`
padding: 8px 16px;
font-size: 14px;
`}
${size === 'large' &&
css`
padding: 16px 32px;
font-size: 18px;
`}
`;
// 最適化されたボタンコンポーネント
const OptimizedButton = styled.button<{
variant: string;
size: string;
}>`
${staticStyles}
${(props) => getDynamicStyles(props.variant, props.size)}
`;
// 使用例
const App = () => {
return (
<div>
<OptimizedButton variant='primary' size='small'>
プライマリボタン
</OptimizedButton>
<OptimizedButton variant='secondary' size='large'>
セカンダリボタン
</OptimizedButton>
</div>
);
};
バンドルサイズの削減方法
Emotion のバンドルサイズを最適化するためのテクニックを紹介します。
typescript// 1. 未使用スタイルの削除
// emotion.config.js
module.exports = {
sourceMap: false, // 本番環境では無効化
autoLabel: 'dev-only', // 開発環境でのみラベルを生成
labelFormat: '[local]',
cssPropOptimization: true, // CSS prop の最適化を有効化
};
// 2. 動的インポートの活用
import { lazy, Suspense } from 'react';
// 重いコンポーネントを遅延読み込み
const HeavyComponent = lazy(
() => import('./HeavyComponent')
);
const App = () => {
return (
<Suspense fallback={<div>読み込み中...</div>}>
<HeavyComponent />
</Suspense>
);
};
// 3. スタイルの共有と再利用
// shared-styles.ts
import { css } from '@emotion/react';
export const commonButtonStyles = css`
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
`;
export const primaryButtonStyles = css`
${commonButtonStyles}
background: #007bff;
color: white;
&:hover {
background: #0056b3;
}
`;
// 4. 条件付きスタイルの最適化
const ConditionalButton = styled.button<{
variant: 'primary' | 'secondary';
}>`
${commonButtonStyles}
${(props) =>
props.variant === 'primary' && primaryButtonStyles}
${(props) =>
props.variant === 'secondary' &&
css`
background: transparent;
color: #007bff;
border: 2px solid #007bff;
&:hover {
background: #007bff;
color: white;
}
`}
`;
実践的なコンポーネント例
ボタンコンポーネントの設計
実際のプロジェクトで使用できる、完全なボタンコンポーネントシステムを構築してみましょう。
typescript// components/Button/types.ts
export interface ButtonProps {
variant?:
| 'primary'
| 'secondary'
| 'outline'
| 'ghost'
| 'danger';
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
loading?: boolean;
disabled?: boolean;
fullWidth?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
children: React.ReactNode;
onClick?: (
event: React.MouseEvent<HTMLButtonElement>
) => void;
type?: 'button' | 'submit' | 'reset';
}
// components/Button/styles.ts
import styled, { css } from '@emotion/styled';
const baseButtonStyles = css`
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
white-space: nowrap;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
&:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
`;
const sizeStyles = {
xs: css`
padding: 4px 8px;
font-size: 12px;
min-height: 24px;
`,
sm: css`
padding: 6px 12px;
font-size: 14px;
min-height: 32px;
`,
md: css`
padding: 8px 16px;
font-size: 16px;
min-height: 40px;
`,
lg: css`
padding: 12px 20px;
font-size: 18px;
min-height: 48px;
`,
xl: css`
padding: 16px 24px;
font-size: 20px;
min-height: 56px;
`,
};
const variantStyles = {
primary: css`
background: #007bff;
color: white;
&:hover:not(:disabled) {
background: #0056b3;
transform: translateY(-1px);
}
&:active:not(:disabled) {
transform: translateY(0);
}
`,
secondary: css`
background: #6c757d;
color: white;
&:hover:not(:disabled) {
background: #545b62;
transform: translateY(-1px);
}
`,
outline: css`
background: transparent;
color: #007bff;
border: 2px solid #007bff;
&:hover:not(:disabled) {
background: #007bff;
color: white;
}
`,
ghost: css`
background: transparent;
color: #007bff;
&:hover:not(:disabled) {
background: rgba(0, 123, 255, 0.1);
}
`,
danger: css`
background: #dc3545;
color: white;
&:hover:not(:disabled) {
background: #c82333;
transform: translateY(-1px);
}
`,
};
export const StyledButton = styled.button<ButtonProps>`
${baseButtonStyles}
${(props) => sizeStyles[props.size || 'md']}
${(props) => variantStyles[props.variant || 'primary']}
${(props) =>
props.fullWidth &&
css`
width: 100%;
`}
${(props) =>
props.loading &&
css`
position: relative;
color: transparent;
&::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`}
`;
// components/Button/Button.tsx
import React from 'react';
import { StyledButton } from './styles';
import { ButtonProps } from './types';
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
fullWidth = false,
leftIcon,
rightIcon,
children,
onClick,
type = 'button',
...props
}) => {
const isDisabled = disabled || loading;
return (
<StyledButton
variant={variant}
size={size}
loading={loading}
disabled={isDisabled}
fullWidth={fullWidth}
onClick={onClick}
type={type}
{...props}
>
{leftIcon && !loading && leftIcon}
{children}
{rightIcon && !loading && rightIcon}
</StyledButton>
);
};
カードコンポーネントの実装
再利用可能で柔軟なカードコンポーネントを実装してみましょう。
typescript// components/Card/types.ts
export interface CardProps {
variant?: 'default' | 'elevated' | 'outlined' | 'filled';
size?: 'sm' | 'md' | 'lg';
interactive?: boolean;
hoverable?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
// components/Card/styles.ts
import styled, { css } from '@emotion/styled';
const baseCardStyles = css`
background: white;
border-radius: 8px;
transition: all 0.3s ease;
overflow: hidden;
`;
const sizeStyles = {
sm: css`
padding: 12px;
`,
md: css`
padding: 16px;
`,
lg: css`
padding: 24px;
`,
};
const variantStyles = {
default: css`
border: 1px solid #e9ecef;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`,
elevated: css`
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
`,
outlined: css`
border: 2px solid #007bff;
box-shadow: none;
`,
filled: css`
border: none;
background: #f8f9fa;
box-shadow: none;
`,
};
export const StyledCard = styled.div<CardProps>`
${baseCardStyles}
${(props) => sizeStyles[props.size || 'md']}
${(props) => variantStyles[props.variant || 'default']}
${(props) =>
props.interactive &&
css`
cursor: pointer;
`}
${(props) =>
props.hoverable &&
css`
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
`}
`;
// components/Card/Card.tsx
import React from 'react';
import { StyledCard } from './styles';
import { CardProps } from './types';
export const Card: React.FC<CardProps> = ({
variant = 'default',
size = 'md',
interactive = false,
hoverable = false,
children,
onClick,
...props
}) => {
return (
<StyledCard
variant={variant}
size={size}
interactive={interactive}
hoverable={hoverable}
onClick={onClick}
{...props}
>
{children}
</StyledCard>
);
};
// カードのサブコンポーネント
export const CardHeader = styled.div`
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e9ecef;
`;
export const CardTitle = styled.h3`
margin: 0;
font-size: 18px;
font-weight: 600;
color: #212529;
`;
export const CardSubtitle = styled.p`
margin: 4px 0 0 0;
font-size: 14px;
color: #6c757d;
`;
export const CardBody = styled.div`
color: #495057;
line-height: 1.6;
`;
export const CardFooter = styled.div`
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
`;
フォームコンポーネントの作成
ユーザビリティを重視したフォームコンポーネントを実装してみましょう。
typescript// components/Form/types.ts
export interface InputProps {
type?:
| 'text'
| 'email'
| 'password'
| 'number'
| 'tel'
| 'url';
placeholder?: string;
value?: string;
defaultValue?: string;
error?: string;
disabled?: boolean;
required?: boolean;
fullWidth?: boolean;
size?: 'sm' | 'md' | 'lg';
onChange?: (
event: React.ChangeEvent<HTMLInputElement>
) => void;
onFocus?: (
event: React.FocusEvent<HTMLInputElement>
) => void;
onBlur?: (
event: React.FocusEvent<HTMLInputElement>
) => void;
}
// components/Form/styles.ts
import styled, { css } from '@emotion/styled';
const baseInputStyles = css`
width: 100%;
border: 2px solid #e9ecef;
border-radius: 6px;
padding: 12px 16px;
font-size: 16px;
transition: all 0.2s ease;
background: white;
&::placeholder {
color: #adb5bd;
}
&:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
&:disabled {
background: #f8f9fa;
cursor: not-allowed;
opacity: 0.6;
}
`;
const sizeStyles = {
sm: css`
padding: 8px 12px;
font-size: 14px;
`,
md: css`
padding: 12px 16px;
font-size: 16px;
`,
lg: css`
padding: 16px 20px;
font-size: 18px;
`,
};
const errorStyles = css`
border-color: #dc3545;
&:focus {
border-color: #dc3545;
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1);
}
`;
export const StyledInput = styled.input<InputProps>`
${baseInputStyles}
${(props) => sizeStyles[props.size || 'md']}
${(props) => props.error && errorStyles}
${(props) =>
!props.fullWidth &&
css`
width: auto;
`}
`;
export const InputWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
export const InputLabel = styled.label`
font-size: 14px;
font-weight: 500;
color: #495057;
`;
export const InputError = styled.span`
font-size: 12px;
color: #dc3545;
margin-top: 4px;
`;
export const InputHelp = styled.span`
font-size: 12px;
color: #6c757d;
margin-top: 4px;
`;
// components/Form/Input.tsx
import React from 'react';
import {
StyledInput,
InputWrapper,
InputLabel,
InputError,
InputHelp,
} from './styles';
import { InputProps } from './types';
interface InputComponentProps extends InputProps {
label?: string;
helpText?: string;
id?: string;
}
export const Input: React.FC<InputComponentProps> = ({
label,
helpText,
error,
id,
...props
}) => {
const inputId =
id ||
`input-${Math.random().toString(36).substr(2, 9)}`;
return (
<InputWrapper>
{label && (
<InputLabel htmlFor={inputId}>
{label}
{props.required && (
<span style={{ color: '#dc3545' }}> *</span>
)}
</InputLabel>
)}
<StyledInput id={inputId} error={error} {...props} />
{error && <InputError>{error}</InputError>}
{helpText && !error && (
<InputHelp>{helpText}</InputHelp>
)}
</InputWrapper>
);
};
// フォームコンテナ
export const Form = styled.form`
display: flex;
flex-direction: column;
gap: 20px;
`;
export const FormRow = styled.div`
display: flex;
gap: 16px;
@media (max-width: 768px) {
flex-direction: column;
}
`;
export const FormGroup = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
`;
まとめ
Emotion と React の組み合わせは、単なる技術的な選択ではありません。開発者の創造性を最大限に引き出し、ユーザー体験を向上させるための強力なツールです。
この記事で紹介したテクニックを実践することで、以下のような効果が期待できます:
開発効率の向上
- コンポーネントとスタイルの密結合により、開発速度が大幅に向上
- 再利用可能なコンポーネントの作成により、コードの重複を削減
- TypeScript との型安全性により、バグの早期発見が可能
保守性の向上
- テーマ機能による一貫性のあるデザイン管理
- コンポーネントベースの設計により、変更の影響範囲を限定
- 明確な命名規則とファイル構成による可読性の向上
パフォーマンスの最適化
- 不要な再レンダリングの回避
- バンドルサイズの最適化
- 効率的なスタイル計算
ユーザー体験の向上
- 一貫性のあるデザインシステム
- スムーズなアニメーションとトランジション
- アクセシビリティへの配慮
Emotion と React の親和性を理解し、実践的なコンポーネント設計の技術を身につけることで、より良い Web アプリケーションを構築できるようになります。この組み合わせの真価は、開発者の創造性を制限するのではなく、それを最大限に引き出すことにあるのです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来