Tailwind CSS マルチブランド設計:CSS 変数と data-theme で横断対応

複数のブランドやテーマを持つ Web サービスを開発していると、ブランドごとに異なる色やデザインを適用したいケースに直面します。
Tailwind CSS と CSS 変数、そして data-theme
属性を組み合わせることで、保守性の高いマルチブランド設計が実現できるのです。
この記事では、実践的なマルチブランド設計の方法を段階的に解説していきますね。
背景
マルチブランド対応の必要性
現代の Web サービスでは、1 つのコードベースで複数のブランドやテーマを切り替えるニーズが増えています。 例えば、ホワイトレーベルサービスや、企業ごとにカスタマイズされた UI を提供する SaaS などが該当します。
従来のアプローチでは、ブランドごとに CSS ファイルを分けたり、条件分岐でクラス名を切り替えたりしていましたが、これらの方法では保守コストが高くなってしまいます。
以下の図は、マルチブランド対応が必要となる典型的なシステム構成を示しています。
mermaidflowchart TB
user1["ブランド A のユーザー"]
user2["ブランド B のユーザー"]
user3["ブランド C のユーザー"]
app["共通アプリケーション<br/>コードベース"]
theme1["テーマ A<br/>カラー/デザイン"]
theme2["テーマ B<br/>カラー/デザイン"]
theme3["テーマ C<br/>カラー/デザイン"]
user1 -->|アクセス| app
user2 -->|アクセス| app
user3 -->|アクセス| app
app -->|適用| theme1
app -->|適用| theme2
app -->|適用| theme3
この図から分かるように、1 つのコードベースで複数のテーマを管理し、ユーザーごとに適切なブランドを表示する仕組みが必要になります。
Tailwind CSS の制約
Tailwind CSS は Utility-First なアプローチで開発効率を高めてくれますが、デフォルトの設定ではブランドごとの色変更に対応しにくいという課題があります。 設定ファイルで定義した色は静的なため、実行時に動的に変更することが難しいのです。
課題
従来のアプローチの問題点
マルチブランド対応における従来のアプローチには、以下のような問題点がありました。
1. CSS ファイルの分割管理
ブランドごとに別々の CSS ファイルを作成する方法では、重複コードが増え、保守性が低下します。 新しいコンポーネントを追加する際には、すべてのブランド用 CSS を更新する必要があり、更新漏れのリスクも高まります。
2. 条件分岐による クラス名の切り替え
JavaScript でブランドを判定し、クラス名を動的に切り替える方法も一般的ですが、コンポーネント内に条件分岐が散在してしまいます。 これにより、コードの可読性が下がり、バグの温床になりやすくなるのです。
3. CSS-in-JS の複雑化
CSS-in-JS ライブラリを使用してブランドごとのスタイルを管理する方法もありますが、Tailwind CSS との併用では設計が複雑になります。 また、ビルドサイズの増加やパフォーマンスへの影響も懸念されます。
以下の図は、従来のアプローチにおける課題を整理したものです。
mermaidflowchart LR
approach1["CSS ファイル分割"]
approach2["条件分岐<br/>クラス切り替え"]
approach3["CSS-in-JS"]
problem1["重複コード増加<br/>保守性低下"]
problem2["可読性低下<br/>バグリスク"]
problem3["複雑な設計<br/>パフォーマンス懸念"]
approach1 -->|課題| problem1
approach2 -->|課題| problem2
approach3 -->|課題| problem3
これらの問題を解決するために、CSS 変数と data-theme
属性を活用したアプローチが有効になります。
理想的な設計要件
マルチブランド設計において、以下の要件を満たすことが理想的です。
- 単一のコードベース - コンポーネントコードはブランドに依存しない
- 動的な切り替え - 実行時にブランドを変更できる
- 型安全性 - TypeScript による型チェックが可能
- 保守性 - ブランド追加時の変更箇所が最小限
- パフォーマンス - ビルドサイズとランタイムパフォーマンスへの影響が少ない
解決策
CSS 変数と data-theme の組み合わせ
CSS 変数(カスタムプロパティ)と HTML の data-*
属性を組み合わせることで、上記の課題を解決できます。
この方法では、ブランドごとの色定義を CSS 変数として管理し、data-theme
属性の値に応じて変数の値を切り替えるのです。
以下の図は、CSS 変数と data-theme
を使った設計の全体像を示しています。
mermaidflowchart TB
html["HTML ルート要素<br/>data-theme='brand-a'"]
cssVars["CSS 変数定義<br/>--primary: #xxx<br/>--secondary: #yyy"]
tailwind["Tailwind 設定<br/>colors: { primary: 'var(--primary)' }"]
component["コンポーネント<br/>className='bg-primary'"]
render["レンダリング結果<br/>背景色: #xxx"]
html -->|適用| cssVars
cssVars -->|参照| tailwind
tailwind -->|利用| component
component -->|出力| render
この仕組みにより、コンポーネントコードを変更することなく、data-theme
属性を変更するだけでブランドの切り替えが可能になります。
実装の全体像
実装は以下のステップで進めていきます。
- グローバル CSS で CSS 変数を定義
- Tailwind 設定ファイルで CSS 変数を参照
- React コンポーネントで
data-theme
を設定 - TypeScript で型定義を追加
それぞれのステップを詳しく見ていきましょう。
具体例
ステップ 1: CSS 変数の定義
まず、グローバル CSS ファイルでブランドごとの CSS 変数を定義します。
このファイルでは、ルート要素に対してデフォルトの色を設定し、data-theme
属性の値に応じて変数を上書きします。
以下は、CSS 変数の基本的な定義例です。
css/* globals.css */
/* デフォルトテーマ(ブランド A)の色定義 */
:root {
--color-primary: 59 130 246; /* blue-500 */
--color-secondary: 139 92 246; /* violet-500 */
--color-accent: 236 72 153; /* pink-500 */
--color-success: 34 197 94; /* green-500 */
--color-warning: 251 146 60; /* orange-400 */
--color-danger: 239 68 68; /* red-500 */
--color-background: 255 255 255; /* white */
--color-surface: 249 250 251; /* gray-50 */
--color-text-primary: 17 24 39; /* gray-900 */
--color-text-secondary: 107 114 128; /* gray-500 */
}
ここで重要なのは、色の値を RGB の数値で記述している点です。
これにより、Tailwind CSS の不透明度ユーティリティ(例: bg-primary/50
)が正しく動作するようになります。
次に、ブランド B 用のテーマを定義します。
css/* ブランド B のテーマ定義 */
[data-theme='brand-b'] {
--color-primary: 16 185 129; /* emerald-500 */
--color-secondary: 14 165 233; /* sky-500 */
--color-accent: 245 158 11; /* amber-500 */
--color-success: 132 204 22; /* lime-500 */
--color-warning: 251 191 36; /* amber-400 */
--color-danger: 220 38 38; /* red-600 */
--color-background: 255 255 255;
--color-surface: 240 253 244; /* green-50 */
--color-text-primary: 6 78 59; /* emerald-900 */
--color-text-secondary: 75 85 99; /* gray-600 */
}
続いて、ブランド C のテーマも定義します。
css/* ブランド C のテーマ定義 */
[data-theme='brand-c'] {
--color-primary: 168 85 247; /* purple-500 */
--color-secondary: 244 114 182; /* pink-400 */
--color-accent: 251 146 60; /* orange-400 */
--color-success: 52 211 153; /* emerald-400 */
--color-warning: 234 179 8; /* yellow-500 */
--color-danger: 248 113 113; /* red-400 */
--color-background: 17 24 39; /* gray-900 */
--color-surface: 31 41 55; /* gray-800 */
--color-text-primary: 243 244 246; /* gray-100 */
--color-text-secondary: 156 163 175; /* gray-400 */
}
ブランド C はダークテーマの例として、背景色とテキスト色を反転させています。 このように、CSS 変数を使うことで、ライトテーマとダークテーマの両方を柔軟に定義できるのです。
ステップ 2: Tailwind 設定の更新
次に、Tailwind CSS の設定ファイルで、先ほど定義した CSS 変数を参照するように設定します。 この設定により、Tailwind のユーティリティクラスが CSS 変数を使用するようになります。
以下は、Tailwind 設定ファイルの例です。
typescript// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
// CSS 変数を参照する色定義
primary:
'rgb(var(--color-primary) / <alpha-value>)',
secondary:
'rgb(var(--color-secondary) / <alpha-value>)',
accent: 'rgb(var(--color-accent) / <alpha-value>)',
success:
'rgb(var(--color-success) / <alpha-value>)',
warning:
'rgb(var(--color-warning) / <alpha-value>)',
danger: 'rgb(var(--color-danger) / <alpha-value>)',
},
},
},
plugins: [],
};
export default config;
<alpha-value>
プレースホルダーを使用することで、不透明度の調整が可能になります。
例えば、bg-primary/50
というクラスを使うと、プライマリカラーの 50% 不透明度が適用されるのです。
背景色やテキスト色についても、同様に CSS 変数を参照するよう設定します。
typescript// tailwind.config.ts の続き
theme: {
extend: {
colors: {
// 前述の色定義に追加
background: 'rgb(var(--color-background) / <alpha-value>)',
surface: 'rgb(var(--color-surface) / <alpha-value>)',
},
textColor: {
primary: 'rgb(var(--color-text-primary) / <alpha-value>)',
secondary: 'rgb(var(--color-text-secondary) / <alpha-value>)',
},
},
}
これにより、bg-background
、bg-surface
、text-primary
、text-secondary
といったクラスが利用可能になります。
ステップ 3: テーマ切り替えの実装
React コンポーネントで data-theme
属性を動的に設定する仕組みを実装します。
Context API を使用して、アプリケーション全体でテーマを管理できるようにしましょう。
まず、テーマの型定義を作成します。
typescript// types/theme.ts
// 利用可能なテーマの型定義
export type Theme = 'brand-a' | 'brand-b' | 'brand-c';
// テーマコンテキストの型定義
export interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
}
型定義により、存在しないテーマ名を指定した際にコンパイルエラーで検出できるようになります。
次に、テーマコンテキストを作成します。
typescript// contexts/ThemeContext.tsx
'use client';
import {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from 'react';
import type {
Theme,
ThemeContextType,
} from '@/types/theme';
const ThemeContext = createContext<
ThemeContextType | undefined
>(undefined);
interface ThemeProviderProps {
children: ReactNode;
defaultTheme?: Theme;
}
export function ThemeProvider({
children,
defaultTheme = 'brand-a',
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme);
// テーマ変更時に HTML 要素に data-theme 属性を設定
useEffect(() => {
document.documentElement.setAttribute(
'data-theme',
theme
);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
useEffect
フックを使用して、テーマが変更されるたびに HTML 要素の data-theme
属性を更新します。
これにより、CSS 変数が自動的に切り替わり、画面全体のデザインが変更されるのです。
カスタムフックを作成して、テーマコンテキストへのアクセスを簡単にします。
typescript// contexts/ThemeContext.tsx の続き
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error(
'useTheme must be used within a ThemeProvider'
);
}
return context;
}
このカスタムフックを使用することで、コンポーネント内で簡潔にテーマにアクセスできるようになります。
ステップ 4: ルートレイアウトへの適用
Next.js のルートレイアウトに ThemeProvider を適用します。 これにより、アプリケーション全体でテーマ機能が利用可能になります。
typescript// app/layout.tsx
import { ThemeProvider } from '@/contexts/ThemeContext';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='ja'>
<body>
<ThemeProvider defaultTheme='brand-a'>
{children}
</ThemeProvider>
</body>
</html>
);
}
ルートレイアウトで ThemeProvider を配置することで、すべてのページコンポーネントでテーマ機能が使えるようになります。
ステップ 5: コンポーネントでの利用
実際のコンポーネントで、定義した色を使用してみましょう。 ここでは、ボタンコンポーネントとテーマ切り替えコンポーネントを作成します。
まず、基本的なボタンコンポーネントの例です。
typescript// components/Button.tsx
import { ButtonHTMLAttributes } from 'react';
interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
children: React.ReactNode;
}
export function Button({
variant = 'primary',
children,
className = '',
...props
}: ButtonProps) {
// variant に応じたクラス名を設定
const variantClasses = {
primary: 'bg-primary hover:bg-primary/90 text-white',
secondary:
'bg-secondary hover:bg-secondary/90 text-white',
danger: 'bg-danger hover:bg-danger/90 text-white',
};
return (
<button
className={`
px-4 py-2 rounded-lg font-medium transition-colors
${variantClasses[variant]}
${className}
`}
{...props}
>
{children}
</button>
);
}
このボタンコンポーネントは、bg-primary
などの Tailwind クラスを使用しています。
テーマが切り替わると、これらのクラスが参照する CSS 変数の値が変わり、ボタンの色も自動的に変更されるのです。
次に、テーマを切り替えるためのコンポーネントを作成します。
typescript// components/ThemeSwitcher.tsx
'use client';
import { useTheme } from '@/contexts/ThemeContext';
import type { Theme } from '@/types/theme';
export function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
const themes: { value: Theme; label: string }[] = [
{ value: 'brand-a', label: 'ブランド A' },
{ value: 'brand-b', label: 'ブランド B' },
{ value: 'brand-c', label: 'ブランド C' },
];
return (
<div className='flex gap-2 p-4 bg-surface rounded-lg'>
<span className='text-primary font-medium'>
テーマ選択:
</span>
{themes.map((t) => (
<button
key={t.value}
onClick={() => setTheme(t.value)}
className={`
px-3 py-1 rounded transition-colors
${
theme === t.value
? 'bg-primary text-white'
: 'bg-background text-secondary hover:bg-surface'
}
`}
>
{t.label}
</button>
))}
</div>
);
}
このコンポーネントでは、useTheme
フックを使用して現在のテーマを取得し、ボタンクリックで setTheme
を呼び出してテーマを変更します。
選択中のテーマには bg-primary
クラスが適用され、ブランドカラーで強調表示されるのです。
ステップ 6: 実用的なコンポーネント例
より実践的な例として、カードコンポーネントとダッシュボードページを作成してみましょう。
まず、カードコンポーネントです。
typescript// components/Card.tsx
import { ReactNode } from 'react';
interface CardProps {
title: string;
description: string;
status?: 'success' | 'warning' | 'danger';
children?: ReactNode;
}
export function Card({
title,
description,
status,
children,
}: CardProps) {
// ステータスに応じたアクセントカラーを設定
const statusColors = {
success: 'border-l-success',
warning: 'border-l-warning',
danger: 'border-l-danger',
};
const borderClass = status
? statusColors[status]
: 'border-l-primary';
return (
<div
className={`
bg-surface border-l-4 ${borderClass}
p-6 rounded-lg shadow-sm
`}
>
<h3 className='text-xl font-bold text-primary mb-2'>
{title}
</h3>
<p className='text-secondary mb-4'>{description}</p>
{children}
</div>
);
}
カードコンポーネントでは、左側のボーダーカラーをステータスに応じて変更しています。 テーマが切り替わると、これらの色も自動的に更新されます。
次に、これらのコンポーネントを組み合わせたダッシュボードページの例です。
typescript// app/dashboard/page.tsx
'use client';
import { ThemeSwitcher } from '@/components/ThemeSwitcher';
import { Card } from '@/components/Card';
import { Button } from '@/components/Button';
export default function DashboardPage() {
return (
<div className='min-h-screen bg-background p-8'>
<div className='max-w-6xl mx-auto space-y-6'>
{/* ヘッダー部分 */}
<header className='flex justify-between items-center'>
<h1 className='text-3xl font-bold text-primary'>
マルチブランドダッシュボード
</h1>
<ThemeSwitcher />
</header>
{/* メインコンテンツ */}
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'>
<Card
title='アクティブユーザー'
description='現在ログイン中のユーザー数'
status='success'
>
<p className='text-4xl font-bold text-primary'>
1,234
</p>
</Card>
<Card
title='処理待ちタスク'
description='対応が必要な項目'
status='warning'
>
<p className='text-4xl font-bold text-primary'>
23
</p>
</Card>
<Card
title='エラー件数'
description='過去 24 時間のエラー'
status='danger'
>
<p className='text-4xl font-bold text-primary'>
5
</p>
</Card>
</div>
{/* アクションボタン */}
<div className='flex gap-4'>
<Button variant='primary'>レポート作成</Button>
<Button variant='secondary'>
データエクスポート
</Button>
<Button variant='danger'>緊急停止</Button>
</div>
</div>
</div>
);
}
このダッシュボードでは、テーマ切り替え機能と様々なコンポーネントを組み合わせています。 ThemeSwitcher でテーマを変更すると、カード、ボタン、テキストなど、すべての要素が一斉に新しいブランドカラーに切り替わるのです。
ステップ 7: ローカルストレージとの連携
ユーザーが選択したテーマを永続化するために、ローカルストレージと連携する機能を追加しましょう。 これにより、ページをリロードしても選択したテーマが保持されます。
typescript// contexts/ThemeContext.tsx の更新版
'use client';
import {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from 'react';
import type {
Theme,
ThemeContextType,
} from '@/types/theme';
const ThemeContext = createContext<
ThemeContextType | undefined
>(undefined);
// ローカルストレージのキー名
const THEME_STORAGE_KEY = 'app-theme';
interface ThemeProviderProps {
children: ReactNode;
defaultTheme?: Theme;
}
export function ThemeProvider({
children,
defaultTheme = 'brand-a',
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const [isInitialized, setIsInitialized] = useState(false);
// 初回レンダリング時にローカルストレージからテーマを読み込む
useEffect(() => {
const savedTheme = localStorage.getItem(
THEME_STORAGE_KEY
) as Theme | null;
if (savedTheme) {
setTheme(savedTheme);
}
setIsInitialized(true);
}, []);
// テーマ変更時に HTML 要素とローカルストレージを更新
useEffect(() => {
if (isInitialized) {
document.documentElement.setAttribute(
'data-theme',
theme
);
localStorage.setItem(THEME_STORAGE_KEY, theme);
}
}, [theme, isInitialized]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error(
'useTheme must be used within a ThemeProvider'
);
}
return context;
}
この実装では、isInitialized
ステートを使用して、初回のローカルストレージ読み込みが完了してから DOM 操作を行うようにしています。
これにより、不要な再レンダリングを防ぎ、パフォーマンスを最適化できるのです。
ステップ 8: サーバーサイドでのテーマ対応
Next.js の App Router では、サーバーコンポーネントでもテーマに対応したい場合があります。 クッキーを使用してテーマ情報を保持し、サーバーサイドで読み取る方法を実装しましょう。
まず、テーマをクッキーに保存する機能を追加します。
typescript// utils/theme.ts
import { cookies } from 'next/headers';
import type { Theme } from '@/types/theme';
export const THEME_COOKIE_NAME = 'app-theme';
// サーバーサイドでテーマを取得
export async function getServerTheme(): Promise<Theme> {
const cookieStore = await cookies();
const theme = cookieStore.get(THEME_COOKIE_NAME)
?.value as Theme | undefined;
return theme || 'brand-a';
}
// クライアントサイドでテーマをクッキーに保存
export function setThemeCookie(theme: Theme) {
document.cookie = `${THEME_COOKIE_NAME}=${theme}; path=/; max-age=31536000`;
}
クッキーを使用することで、サーバーサイドでも初回レンダリング時からテーマを適用できます。
次に、ThemeProvider を更新してクッキーにも保存するようにします。
typescript// contexts/ThemeContext.tsx の setTheme 部分を更新
import { setThemeCookie } from '@/utils/theme';
// ThemeProvider 内の setTheme をラップ
const handleSetTheme = (newTheme: Theme) => {
setTheme(newTheme);
setThemeCookie(newTheme);
};
return (
<ThemeContext.Provider
value={{ theme, setTheme: handleSetTheme }}
>
{children}
</ThemeContext.Provider>
);
これにより、クライアントサイドでテーマを変更すると、自動的にクッキーにも保存されます。
高度な活用例
1. レスポンシブなカラーバリエーション
画面サイズに応じて色の濃淡を変更したい場合、Tailwind のレスポンシブ修飾子と組み合わせることができます。
typescript// components/Hero.tsx
export function Hero() {
return (
<div
className='
bg-primary/10 md:bg-primary/20 lg:bg-primary/30
p-8 md:p-12 lg:p-16
rounded-lg
'
>
<h2 className='text-3xl md:text-4xl lg:text-5xl font-bold text-primary'>
レスポンシブヒーローセクション
</h2>
<p className='text-secondary mt-4'>
画面サイズに応じて背景の濃度が変化します。
</p>
</div>
);
}
2. グラデーションの活用
CSS 変数はグラデーションにも活用できます。
css/* globals.css に追加 */
.gradient-brand {
background: linear-gradient(
135deg,
rgb(var(--color-primary)) 0%,
rgb(var(--color-secondary)) 100%
);
}
このクラスを使用すると、テーマに応じたグラデーション背景が作成できます。
typescript// components/GradientCard.tsx
export function GradientCard({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className='gradient-brand p-8 rounded-lg text-white'>
{children}
</div>
);
}
3. ダークモードとの併用
ブランドテーマに加えて、ライト/ダークモードの切り替えも実装できます。
typescript// types/theme.ts に追加
export type ColorMode = 'light' | 'dark';
export interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
colorMode: ColorMode;
setColorMode: (mode: ColorMode) => void;
}
ThemeContext を拡張して、カラーモードの管理機能を追加します。
typescript// contexts/ThemeContext.tsx の拡張
export function ThemeProvider({
children,
defaultTheme = 'brand-a',
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const [colorMode, setColorMode] =
useState<ColorMode>('light');
useEffect(() => {
document.documentElement.setAttribute(
'data-theme',
theme
);
document.documentElement.setAttribute(
'data-color-mode',
colorMode
);
}, [theme, colorMode]);
return (
<ThemeContext.Provider
value={{ theme, setTheme, colorMode, setColorMode }}
>
{children}
</ThemeContext.Provider>
);
}
CSS で data-color-mode
に応じた変数のオーバーライドを定義すれば、ブランドとカラーモードを独立して切り替えられるようになります。
パフォーマンス最適化
1. CSS 変数のスコープ制限
すべての色を CSS 変数にする必要はありません。 変更の可能性がある色のみを CSS 変数にすることで、ブラウザのレンダリング負荷を軽減できます。
css/* グレースケールは固定値を使用 */
:root {
/* ブランド固有の色のみ CSS 変数化 */
--color-primary: 59 130 246;
--color-secondary: 139 92 246;
/* グレースケールは Tailwind のデフォルトを使用 */
}
2. 遅延読み込み
テーマ切り替え UI は、必要になるまで読み込まないようにできます。
typescript// app/dashboard/page.tsx
import dynamic from 'next/dynamic';
// ThemeSwitcher を動的インポート
const ThemeSwitcher = dynamic(
() =>
import('@/components/ThemeSwitcher').then(
(mod) => mod.ThemeSwitcher
),
{ ssr: false }
);
export default function DashboardPage() {
return (
<div>
<ThemeSwitcher />
{/* その他のコンテンツ */}
</div>
);
}
これにより、初期バンドルサイズを削減し、ページの読み込み速度を向上させられます。
テストの実装
マルチブランド設計のテストも重要です。 以下は、Jest と React Testing Library を使用したテストの例になります。
typescript// __tests__/ThemeContext.test.tsx
import {
render,
screen,
fireEvent,
} from '@testing-library/react';
import {
ThemeProvider,
useTheme,
} from '@/contexts/ThemeContext';
function TestComponent() {
const { theme, setTheme } = useTheme();
return (
<div>
<span data-testid='current-theme'>{theme}</span>
<button onClick={() => setTheme('brand-b')}>
ブランド B に変更
</button>
</div>
);
}
describe('ThemeContext', () => {
it('デフォルトテーマが正しく設定される', () => {
render(
<ThemeProvider defaultTheme='brand-a'>
<TestComponent />
</ThemeProvider>
);
expect(
screen.getByTestId('current-theme')
).toHaveTextContent('brand-a');
});
it('テーマを変更できる', () => {
render(
<ThemeProvider defaultTheme='brand-a'>
<TestComponent />
</ThemeProvider>
);
const button = screen.getByText('ブランド B に変更');
fireEvent.click(button);
expect(
screen.getByTestId('current-theme')
).toHaveTextContent('brand-b');
});
it('data-theme 属性が HTML 要素に設定される', () => {
render(
<ThemeProvider defaultTheme='brand-a'>
<TestComponent />
</ThemeProvider>
);
const button = screen.getByText('ブランド B に変更');
fireEvent.click(button);
expect(
document.documentElement.getAttribute('data-theme')
).toBe('brand-b');
});
});
テストを実装することで、テーマ切り替え機能が期待通りに動作することを保証できます。
まとめ
CSS 変数と data-theme
属性を組み合わせることで、Tailwind CSS でのマルチブランド設計を効率的に実現できました。
この方法には以下のようなメリットがあります。
# | メリット | 詳細 |
---|---|---|
1 | 単一コードベース | コンポーネントコードはブランドに依存せず、保守性が大幅に向上します |
2 | 実行時の動的切り替え | JavaScript でテーマを変更するだけで、即座に UI が更新されます |
3 | 型安全性 | TypeScript による型チェックで、存在しないテーマの指定を防げます |
4 | 拡張性 | 新しいブランドの追加は CSS 変数の定義と型の追加だけで済みます |
5 | パフォーマンス | CSS 変数のみで実装するため、ビルドサイズやランタイムへの影響が最小限です |
6 | Tailwind との親和性 | Tailwind の全機能(不透明度、レスポンシブなど)がそのまま利用できます |
実装のポイントをまとめると、以下のようになります。
1. CSS 変数の定義
- RGB 形式で色を定義し、不透明度に対応
:root
でデフォルト値を設定[data-theme]
セレクタでブランドごとに上書き
2. Tailwind 設定
rgb(var(--color-name) / <alpha-value>)
形式で参照- 必要な色のみを CSS 変数化し、パフォーマンスを最適化
3. React での実装
- Context API でテーマ状態を管理
useEffect
でdata-theme
属性を更新- ローカルストレージやクッキーで永続化
4. コンポーネント設計
- ブランド固有の値をハードコードしない
- Tailwind のユーティリティクラスを活用
- 型定義で安全性を確保
この設計パターンは、スケーラブルで保守性の高いマルチブランド対応を実現する強力な手法です。 ホワイトレーベルサービスや企業向けカスタマイズ機能など、様々な場面で活用できるでしょう。
ぜひ、あなたのプロジェクトでもこの方法を試してみてください。 最初は少し複雑に感じるかもしれませんが、一度設定してしまえば、新しいブランドの追加や変更が驚くほど簡単になりますよ。
関連リンク
- article
Tailwind CSS マルチブランド設計:CSS 変数と data-theme で横断対応
- article
Tailwind CSS バリアント辞典:aria-[]/data-[]/has-[]/supports-[] を一気に使い倒す
- article
Tailwind CSS を macOS で最短導入:Yarn PnP・PostCSS・ESLint 連携レシピ
- article
Tailwind CSS と UnoCSS/Windi CSS を徹底比較:ビルド速度・DX・互換性
- article
Tailwind CSS が反映されない時の総点検:content 設定・JIT・パージの落とし穴
- article
Tailwind CSS のユーティリティ設計を図で直感理解:原子化・コンポジション・トークンの関係
- article
Vue.js コンポーネント API 設計:props/emit/slot を最小 API でまとめる
- article
GitHub Copilot 前提のコーディング設計:コメント駆動 → テスト → 実装の最短ループ
- article
Tailwind CSS マルチブランド設計:CSS 変数と data-theme で横断対応
- article
Svelte フォーム体験設計:Optimistic UI/エラー復旧/再送戦略の型
- article
GitHub Actions でゼロダウンタイムリリース:canary/blue-green をパイプライン実装
- article
Git エイリアス 50 連発:長コマンドを一行にする仕事術まとめ
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来