Emotion でユーティリティクラスを自作する方法

React アプリケーションのスタイリングにおいて、Emotion は CSS-in-JS ライブラリとして非常に人気があります。しかし、実際の開発で使用していると「同じようなスタイルを何度も書いている」「チーム内でスタイルの統一が難しい」といった課題に直面することはありませんか。
今回は、Emotion を使用してユーティリティクラスを自作し、効率的で保守性の高いスタイリング環境を構築する方法をご紹介いたします。この方法を活用することで、開発効率が格段に向上し、コードの可読性も大幅に改善されることでしょう。
背景
Emotion におけるスタイリングの現状
現在多くの React プロジェクトで採用されている Emotion は、JavaScript 内で CSS を記述できる強力なライブラリです。従来の CSS ファイルとは異なり、コンポーネントと密接に結びついたスタイリングが可能になります。
typescriptimport { css } from '@emotion/react'
const Button = () => (
<button
css={css`
padding: 12px 24px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
`}
>
クリック
</button>
)
このような書き方は非常に直感的で、コンポーネントごとに独立したスタイルを定義できる点が魅力的です。しかし、実際のプロジェクトでは、同様のスタイル定義を複数の場所で繰り返し記述することが多くなってしまいます。
ユーティリティクラスの必要性とメリット
ユーティリティクラスは、再利用可能な小さなスタイル単位として設計されたものです。Tailwind CSS のように、マージンやパディング、フォントサイズなどの基本的なスタイルをクラス単位で管理することで、以下のようなメリットが得られます。
まず、開発速度の向上が挙げられます。よく使用するスタイルパターンを事前に定義しておくことで、毎回同じスタイルを書く手間が省けますね。
次に、一貫性の確保も重要なポイントです。チーム開発において、デザインシステムに基づいた統一されたスタイルを適用することが容易になります。
CSS-in-JS での課題点
Emotion を含む CSS-in-JS ライブラリには多くの利点がある一方で、いくつかの課題も存在します。
最も顕著な課題は、スタイルの重複問題です。コンポーネントごとにスタイルを定義するため、似たようなスタイル定義が複数箇所に散らばってしまうことがあります。また、プロジェクトが大規模になると、どこでどのようなスタイルが定義されているかを把握することが困難になってしまいます。
さらに、実行時にスタイルが生成されるため、パフォーマンスへの影響も考慮する必要があるでしょう。
課題
繰り返し使用するスタイルの管理問題
実際の開発現場では、マージンやパディングといった基本的なスタイルを何度も記述することになります。例えば、以下のようなコードが複数のコンポーネントに散らばっているのをよく見かけませんか。
typescript// コンポーネント A
const ComponentA = () => (
<div
css={css`
margin: 16px 0;
padding: 24px;
border-radius: 8px;
`}
>
内容 A
</div>
)
// コンポーネント B
const ComponentB = () => (
<div
css={css`
margin: 16px 0;
padding: 24px;
border-radius: 8px;
`}
>
内容 B
</div>
)
このように、同じスタイル定義が複数箇所に存在する状況は、保守性の観点から大きな問題となります。デザインの変更が必要になった際、すべての箇所を手動で修正しなければならず、見落としによるバグの原因にもなってしまいますね。
保守性とコードの可読性の向上
大規模なプロジェクトにおいて、スタイルの管理は複雑化の一途をたどります。特に以下のような課題が顕在化することが多いでしょう。
スタイルの散在問題: 関連するスタイルが複数のファイルに分散し、全体の把握が困難になります。
命名の統一性: 同じような機能を持つスタイルでも、開発者によって異なる命名規則や実装方法が採用されてしまうことがあります。
デバッグの困難さ: 問題のあるスタイルがどこで定義されているかを特定するのに時間がかかってしまいます。
チーム開発での一貫性確保
複数人でのチーム開発では、個々の開発者がそれぞれ異なるスタイリング手法を採用してしまう可能性があります。
typescript// 開発者Aのスタイル
const buttonStyleA = css`
padding: 12px 20px;
font-size: 14px;
`;
// 開発者Bのスタイル
const buttonStyleB = css`
padding: 10px 24px;
font-size: 16px;
`;
このような状況では、UI の一貫性が損なわれ、ユーザビリティの低下につながってしまいます。また、デザインシステムの導入や変更が困難になり、プロジェクトの拡張性に悪影響を与えることになるでしょう。
解決策
Emotion でのユーティリティクラス設計パターン
これらの課題を解決するため、Emotion においてユーティリティクラスを設計するパターンをご紹介します。基本的な考え方は、よく使用されるスタイルパターンを関数として抽象化し、再利用可能な形で提供することです。
まず、スペーシング(マージンとパディング)のユーティリティから始めてみましょう。
typescriptimport { css } from '@emotion/react'
// スペーシングスケールの定義
const spacing = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
'2xl': '48px',
} as const
// マージンユーティリティの実装
export const margin = {
all: (size: keyof typeof spacing) => css`
margin: ${spacing[size]};
`,
vertical: (size: keyof typeof spacing) => css`
margin-top: ${spacing[size]};
margin-bottom: ${spacing[size]};
`,
horizontal: (size: keyof typeof spacing) => css`
margin-left: ${spacing[size]};
margin-right: ${spacing[size]};
`,
}
このパターンにより、統一されたスペーシングシステムを構築できます。各開発者が独自の値を使用することなく、デザインシステムに基づいた一貫性のあるスタイルを適用することが可能になりますね。
css プロパティと styled コンポーネントの使い分け
Emotion では、css
プロパティと styled
コンポーネントという 2 つの主要なアプローチがあります。ユーティリティクラスの実装においては、用途に応じてこれらを適切に使い分けることが重要です。
css プロパティは、個別のスタイル適用に適しています。
typescriptimport { css } from '@emotion/react'
import { margin } from './utilities'
const Component = () => (
<div css={[margin.vertical('md'), margin.horizontal('lg')]}>
内容
</div>
)
styled コンポーネントは、再利用性の高いコンポーネント作成に適用されます。
typescriptimport styled from '@emotion/styled'
const Container = styled.div<{ spacing?: keyof typeof spacing }>`
${({ spacing = 'md' }) => margin.all(spacing)}
border-radius: 8px;
background-color: #f5f5f5;
`
型安全なユーティリティクラスの作成方法
TypeScript と組み合わせることで、型安全なユーティリティクラスを実装できます。これにより、コンパイル時にスタイルの誤りを検出し、開発効率の向上と品質保証を同時に実現できるでしょう。
typescript// カラーパレットの型安全な定義
const colors = {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
900: '#1e3a8a',
},
secondary: {
50: '#f8fafc',
500: '#64748b',
900: '#0f172a',
},
} as const
// カラーユーティリティの型安全な実装
type ColorKey = keyof typeof colors
type ColorShade = keyof typeof colors[ColorKey]
export const backgroundColor = (
color: ColorKey,
shade: ColorShade
) => css`
background-color: ${colors[color][shade]};
`
この実装により、存在しないカラー名や階調を指定した場合にコンパイルエラーが発生し、ランタイムでのエラーを未然に防ぐことができます。
具体例
基本的なスペーシングユーティリティの実装
それでは、実践的なスペーシングユーティリティシステムを構築してみましょう。以下の実装例では、Tailwind CSS のスペーシングシステムを参考にした包括的なユーティリティを作成します。
typescript// utils/spacing.ts
import { css } from '@emotion/react'
// スペーシングスケールの定義
const spacingScale = {
0: '0px',
1: '4px',
2: '8px',
3: '12px',
4: '16px',
5: '20px',
6: '24px',
8: '32px',
10: '40px',
12: '48px',
16: '64px',
20: '80px',
24: '96px',
32: '128px',
} as const
type SpacingSize = keyof typeof spacingScale
次に、マージンとパディングの各方向に対応したユーティリティ関数を実装します。
typescript// マージンユーティリティの実装
export const margin = {
// 全方向のマージン
all: (size: SpacingSize) => css`
margin: ${spacingScale[size]};
`,
// 上下方向のマージン
y: (size: SpacingSize) => css`
margin-top: ${spacingScale[size]};
margin-bottom: ${spacingScale[size]};
`,
// 左右方向のマージン
x: (size: SpacingSize) => css`
margin-left: ${spacingScale[size]};
margin-right: ${spacingScale[size]};
`,
// 個別方向のマージン
top: (size: SpacingSize) => css`
margin-top: ${spacingScale[size]};
`,
right: (size: SpacingSize) => css`
margin-right: ${spacingScale[size]};
`,
bottom: (size: SpacingSize) => css`
margin-bottom: ${spacingScale[size]};
`,
left: (size: SpacingSize) => css`
margin-left: ${spacingScale[size]};
`,
}
パディングについても同様の構造で実装できます。
typescript// パディングユーティリティの実装
export const padding = {
all: (size: SpacingSize) => css`
padding: ${spacingScale[size]};
`,
y: (size: SpacingSize) => css`
padding-top: ${spacingScale[size]};
padding-bottom: ${spacingScale[size]};
`,
x: (size: SpacingSize) => css`
padding-left: ${spacingScale[size]};
padding-right: ${spacingScale[size]};
`,
top: (size: SpacingSize) => css`
padding-top: ${spacingScale[size]};
`,
}
レスポンシブ対応ユーティリティクラス
現代の Web 開発では、レスポンシブデザインは必須要件です。Emotion でレスポンシブ対応のユーティリティクラスを実装してみましょう。
typescript// utils/responsive.ts
const breakpoints = {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
} as const
type Breakpoint = keyof typeof breakpoints
// メディアクエリヘルパー関数
const media = (breakpoint: Breakpoint) => `@media (min-width: ${breakpoints[breakpoint]})`
レスポンシブ対応のスペーシングユーティリティを実装します。
typescript// レスポンシブマージンユーティリティ
export const responsiveMargin = {
all: (sizes: Partial<Record<Breakpoint | 'base', SpacingSize>>) => css`
${sizes.base && `margin: ${spacingScale[sizes.base]};`}
${sizes.sm && css`
${media('sm')} {
margin: ${spacingScale[sizes.sm]};
}
`}
${sizes.md && css`
${media('md')} {
margin: ${spacingScale[sizes.md]};
}
`}
${sizes.lg && css`
${media('lg')} {
margin: ${spacingScale[sizes.lg]};
}
`}
`,
}
// 使用例
const ResponsiveComponent = () => (
<div
css={responsiveMargin.all({
base: 4, // 16px
md: 6, // 24px
lg: 8, // 32px
})}
>
レスポンシブマージンの適用
</div>
)
カスタムテーマとの連携方法
デザインシステムとの一貫性を保つため、テーマプロバイダーと連携したユーティリティシステムを構築してみましょう。
typescript// theme/index.ts
export const theme = {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
900: '#1e3a8a',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
500: '#6b7280',
900: '#111827',
},
},
spacing: spacingScale,
borderRadius: {
none: '0px',
sm: '2px',
md: '6px',
lg: '8px',
xl: '12px',
full: '9999px',
},
fontSize: {
xs: '12px',
sm: '14px',
base: '16px',
lg: '18px',
xl: '20px',
'2xl': '24px',
'3xl': '30px',
},
} as const
テーマを活用したユーティリティクラスを実装します。
typescript// utils/theme-utilities.ts
import { css } from '@emotion/react'
import { theme } from '../theme'
type ColorKey = keyof typeof theme.colors
type ColorShade = keyof typeof theme.colors[ColorKey]
type BorderRadiusKey = keyof typeof theme.borderRadius
type FontSizeKey = keyof typeof theme.fontSize
// カラーユーティリティ
export const backgroundColor = (color: ColorKey, shade: ColorShade) => css`
background-color: ${theme.colors[color][shade]};
`
export const textColor = (color: ColorKey, shade: ColorShade) => css`
color: ${theme.colors[color][shade]};
`
// 境界線ユーティリティ
export const borderRadius = (radius: BorderRadiusKey) => css`
border-radius: ${theme.borderRadius[radius]};
`
// タイポグラフィユーティリティ
export const fontSize = (size: FontSizeKey) => css`
font-size: ${theme.fontSize[size]};
`
実際のコンポーネントでの使用例をご紹介します。
typescript// components/Card.tsx
import React from 'react'
import { css } from '@emotion/react'
import { padding, margin } from '../utils/spacing'
import { backgroundColor, borderRadius, textColor } from '../utils/theme-utilities'
const Card: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const cardStyles = css`
${backgroundColor('gray', 50)}
${borderRadius('lg')}
${padding.all(6)}
${margin.bottom(4)}
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
`
return (
<div css={cardStyles}>
{children}
</div>
)
}
// 使用方法
const App = () => (
<div css={margin.all(8)}>
<Card>
<h2 css={[fontSize('2xl'), textColor('gray', 900), margin.bottom(2)]}>
カードタイトル
</h2>
<p css={[fontSize('base'), textColor('gray', 500)]}>
カードの内容がここに表示されます。
</p>
</Card>
</div>
)
まとめ
今回は、Emotion を使用したユーティリティクラスの自作方法について詳しくご紹介いたしました。従来の CSS-in-JS におけるスタイルの重複や保守性の課題を、体系的なユーティリティクラスシステムで解決することができましたね。
実装のポイント
型安全性の確保: TypeScript と組み合わせることで、コンパイル時にスタイルの誤りを検出し、開発効率と品質の両立を実現できます。
再利用可能な設計: スペーシング、カラー、タイポグラフィなどの基本要素を関数として抽象化することで、コードの重複を大幅に削減できました。
レスポンシブ対応: メディアクエリを組み込んだユーティリティクラスにより、デバイスに応じた柔軟なスタイリングが可能になります。
導入効果
このアプローチを採用することで、以下のような効果が期待できるでしょう。
- 開発速度の向上: 事前に定義されたユーティリティクラスにより、スタイリング作業の効率が格段に向上します
- 一貫性の保持: デザインシステムに基づいた統一されたスタイルを、チーム全体で共有することができます
- 保守性の改善: スタイルの変更が必要な場合も、ユーティリティクラスを修正するだけで全体に反映されます
今後の拡張
基本的なユーティリティシステムができあがったら、プロジェクトの成長に合わせて以下のような機能を追加していくとよいでしょう。
グリッドシステムやフレックスボックス用のユーティリティ、アニメーション・トランジション用のユーティリティ、そしてダークモード対応などの高度な機能も段階的に実装していくことができます。
Emotion のユーティリティクラス自作により、効率的で保守性の高い React アプリケーションの開発が実現できることでしょう。ぜひ今回ご紹介した手法を、あなたのプロジェクトでも活用してみてくださいね。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来