CSS-in-JS vs CSS Modules – スタイリング手法の比較と使い分け
フロントエンド開発における スタイリングの課題
React を使ったプロジェクトで、「CSS クラス名の競合を避けたい」「コンポーネントごとにスタイルを管理したい」「パフォーマンスと保守性のバランスが取りたい」という課題に直面したことはありませんか?
今回は、CSS Modules、CSS-in-JS(Styled Components、Emotion)、Tailwind CSS という3つのスタイリング手法を比較し、それぞれのメリット・デメリット、そして選択基準をまとめておきたいと思います。
対象となる方
- React でのスタイリング方法に悩んでいる方
- 新しいプロジェクト・既存プロジェクトでのスタイリング手法の検討
- チームのスタイリング戦略を決める開発リーダー
※ このドキュメントは React 18、TypeScript で書いていきます。
CSS Modules – スコープ付き CSS
CSS Modules の仕組み
CSS Modules は、CSS ファイルをモジュールとして import し、クラス名を自動で一意にスコープしてくれます。ビルド時にクラス名が変換され、競合を防ぎます。
Button.tsx と Button.module.css をセットで作成
/* Button.module.css */
.button {
background-color: #0066cc;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button:hover {
background-color: #0052a3;
}
.button--primary {
background-color: #ff6b35;
}
.button--disabled {
opacity: 0.5;
cursor: not-allowed;
}
// Button.tsx
import styles from './Button.module.css';
interface ButtonProps {
variant?: 'primary' | 'default';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({ variant = 'default', disabled, onClick, children }: ButtonProps) {
const className = [
styles.button,
variant === 'primary' && styles['button--primary'],
disabled && styles['button--disabled'],
]
.filter(Boolean)
.join(' ');
return (
<button className={className} onClick={onClick} disabled={disabled}>
{children}
</button>
);
}
CSS Modules のメリット・デメリット
| メリット | デメリット |
| スコープが自動でユニーク(クラス名競合なし) | 複雑な条件付きスタイルが書きにくい |
| CSS の標準機能を活用(学習コスト低) | 動的なスタイル値(色、サイズ)の実装が手間 |
| パフォーマンス最適化(CSS ファイル分割可) | CSS と JavaScript ファイルを分ける必要 |
| ブラウザで標準サポート | BEM などの命名規則が必須 |
CSS Modules の適用場面
- 従来の CSS に習熟しているチーム
- CSS ファイルの最適化が重要なプロジェクト
- デザインシステムが安定している場合
- 複雑な条件付きスタイルが少ない
CSS-in-JS:Styled Components と Emotion
CSS-in-JS の基本
CSS-in-JS は、JavaScript でスタイルを定義し、動的にスタイルを生成します。Styled Components と Emotion が代表的です。
npm install styled-components
npm install --save-dev @types/styled-components
// Button.tsx
import styled from 'styled-components';
const StyledButton = styled.button`
background-color: #0066cc;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #0052a3;
}
${(props) =>
props.variant === 'primary' &&
`
background-color: #ff6b35;
`}
${(props) =>
props.disabled &&
`
opacity: 0.5;
cursor: not-allowed;
`}
`;
interface ButtonProps {
variant?: 'primary' | 'default';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({ variant = 'default', disabled, onClick, children }: ButtonProps) {
return (
<StyledButton variant={variant} disabled={disabled} onClick={onClick}>
{children}
</StyledButton>
);
}
Emotion での実装
npm install @emotion/react @emotion/styled
import styled from '@emotion/styled';
const StyledButton = styled.button`
background-color: ${(props: any) => (props.variant === 'primary' ? '#ff6b35' : '#0066cc')};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: ${(props: any) =>
props.variant === 'primary' ? '#ff8c42' : '#0052a3'};
}
${(props: any) =>
props.disabled &&
`
opacity: 0.5;
cursor: not-allowed;
`}
`;
export function Button({ variant = 'default', disabled, onClick, children }: ButtonProps) {
return (
<StyledButton variant={variant} disabled={disabled} onClick={onClick}>
{children}
</StyledButton>
);
}
CSS-in-JS のメリット・デメリット
| メリット | デメリット |
| 動的なスタイル値が簡単(props, 変数) | Runtime CSS 生成(パフォーマンスオーバーヘッド) |
| JavaScript でスタイルロジック統一 | バンドルサイズ増(ライブラリ込み) |
| スコープ自動化(クラス名競合なし) | CSS-in-JS ライブラリの学習コスト |
| コンポーネント + スタイルを同じファイルで | IDE の CSS 補完が弱い場合がある |
CSS-in-JS の適用場面
- 動的なスタイル(props に応じた色変更など)が多い
- デザイントークン(色、サイズなど)の一元管理
- コンポーネント + スタイルを同じファイルで管理したい
- テーマ切り替え(ライト/ダーク モード)の実装
Tailwind CSS – ユーティリティファースト
Tailwind CSS の仕組み
Tailwind CSS は、事前に定義されたユーティリティクラスを HTML に適用するアプローチです。CSS ファイルを書かず、クラス名の組み合わせでデザインを構築します。
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// Button.tsx
interface ButtonProps {
variant?: 'primary' | 'default';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({
variant = 'default',
disabled,
onClick,
children,
}: ButtonProps) {
const baseClasses =
'px-5 py-2 border-none rounded cursor-pointer text-white font-medium';
const variantClasses =
variant === 'primary'
? 'bg-orange-500 hover:bg-orange-600'
: 'bg-blue-600 hover:bg-blue-700';
const disabledClasses = disabled ? 'opacity-50 cursor-not-allowed' : '';
return (
<button
className={`${baseClasses} ${variantClasses} ${disabledClasses}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
clsx ライブラリで可読性向上
npm install clsx
import clsx from 'clsx';
export function Button({
variant = 'default',
disabled,
onClick,
children,
}: ButtonProps) {
return (
<button
className={clsx(
'px-5 py-2 border-none rounded cursor-pointer text-white font-medium',
{
'bg-orange-500 hover:bg-orange-600': variant === 'primary',
'bg-blue-600 hover:bg-blue-700': variant === 'default',
'opacity-50 cursor-not-allowed': disabled,
}
)}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
Tailwind CSS のメリット・デメリット
| メリット | デメリット |
| CSS ファイルを書かない(生産性向上) | クラス名が HTML に長く記述される |
| デザイン一貫性(事前定義トークン) | IDE の補完が必須 |
| CSS ファイル削減(PurgeCSS 対応) | 複雑なカスタマイズは別途 CSS 必要 |
| ビルド最適化(不要クラスを削除) | 学習コスト(クラス名の暗記) |
Tailwind CSS の適用場面
- 開発速度を最優先したいプロジェクト
- デザイントークンが厳格に定義されている
- スタートアップ・プロトタイプ
- 複雑なカスタムスタイルが少ない
3つのアプローチの比較表
| 観点 | CSS Modules | CSS-in-JS | Tailwind |
| 学習コスト | 低 | 中 | 中 |
| 開発速度 | 普通 | 速い | 最速 |
| パフォーマンス | 優秀 | 中 | 優秀 |
| スコープ管理 | 自動 | 自動 | 手動 |
| 動的スタイル | 難 | 簡単 | 中 |
| ファイル構成 | 分離 | 統一 | HTML に集中 |
| カスタマイズ性 | 高 | 高 | 中 |
プロジェクト規模・チーム構成別の選択基準
小規模プロジェクト(1-2 人、短期間)
推奨:Tailwind CSS
- 開発速度が最優先
- スタートアップ・プロトタイプに最適
- デザイン決定が少なくて済む
中規模プロジェクト(3-10 人チーム)
推奨:CSS-in-JS(Styled Components)または Tailwind + CSS Modules
- 複雑なコンポーネントが増えてくる
- テーマ切り替え(ライト/ダーク モード)が必要
- チーム内で命名規則の統一が重要
大規模プロジェクト(10+ 人チーム)
推奨:CSS Modules + デザイントークン、または デザインシステム
- 保守性・パフォーマンスが最優先
- デザインシステムが確立されている
- 複数チームでの分業が必要
実装例:Design System での統一
CSS Modules + Design Tokens
// tokens.ts
export const colors = {
primary: '#0066cc',
secondary: '#ff6b35',
gray: '#333333',
lightGray: '#f5f5f5',
};
export const spacing = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
};
export const typography = {
headingLarge: '24px',
body: '16px',
caption: '12px',
};
/* Button.module.css */
@import './tokens.css';
.button {
background-color: var(--color-primary);
color: white;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--typography-body);
}
ハイブリッドアプローチ:Tailwind + CSS Modules
単純なコンポーネントは Tailwind、複雑な部分は CSS Modules という混合アプローチもあります。
import styles from './Card.module.css';
export function Card({ children, variant = 'default' }: any) {
return (
<div className={`p-4 rounded-lg ${styles[`card--${variant}`]}`}>
{children}
</div>
);
}
まとめ
以上で CSS スタイリング手法の比較を終えたいと思います。
CSS Modules、CSS-in-JS、Tailwind CSS には、それぞれ異なるアプローチと強みがあります。「どれが最高か」ではなく、「プロジェクトの要件に最適か」を基準に選択することが重要です。小規模プロジェクトなら Tailwind で速度重視、大規模チームなら CSS Modules で保守性重視、といった具合に、柔軟に判断してください。
スタイリング手法の選択は、プロジェクト成功を左右する大きな決定です。チーム全体で合意のうえ、最適なアプローチを選択していきましょう。
