チュートリアル

React での色テーミング:CSS 変数とコンテキスト

5分で読める

テーミングはシンプルに見える問題の 1 つですが、スケール時に維持する必要があります。光とダーク モード間の単一の切り替えは簡単です。複数のブランドを提供する製品、ユーザーカスタマイズ可能なパレット、ページ リロードなしでテーマを直ちに切り替える必要があります。より意図的なアーキテクチャが必要です。

React のコンポーネント モデルと CSS カスタム プロパティはテーミング用の自然なペアリングです。CSS 変数は色値を宣言的に処理。React コンテキストはテーマ状態を管理。Tailwind CSS(最新プロジェクト)両方をブリッジします。このガイドは各レイヤーをカバーします - テーマ アーキテクチャを構造化する方法、CSS 変数基盤を実装し、React 状態を接続し、マルチ ブランド シナリオを処理する方法。


テーマ アーキテクチャ パターン

パターン 1:CSS 変数のみ(JavaScript 状態なし)

基本的なダーク モード サポートのための最も単純なパターンは React 状態を全くする必要がありません。prefers-color-scheme メディア クエリはカスタム プロパティ値を変更し、すべてのコンポーネントは自動的に更新:

/* globals.css */
:root {
  --color-bg: #F8FAFC;
  --color-text: #1E293B;
  --color-brand: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0F172A;
    --color-text: #F1F5F9;
    --color-brand: #60A5FA;
  }
}

コンポーネント var(--color-bg) を使用し、現在のテーマについて知る必要はありません。ブラウザはすべてを処理します。

使用する場合: OS レベルの設定を尊重する必要があり、アプリ内でユーザー controlled 切り替えが不要なサイト。

制限: ユーザーがアプリ内の OS 設定をオーバーライドする方法はありません。

パターン 2:データ属性テーマ切り替え

data-theme 属性を <html> 要素に追加して、アクティブなテーマを明示的にするか、オーバーライド可能にします。これは SSR と互換性があり、フラッシュを避け、localStorage 永続化と自明に結合:

/* Default: light */
[data-theme="light"],
:root {
  --color-bg: #F8FAFC;
  --color-text: #1E293B;
  --color-brand: #2563EB;
  --color-surface: #FFFFFF;
  --color-border: #E2E8F0;
}

/* Dark */
[data-theme="dark"] {
  --color-bg: #0F172A;
  --color-text: #F1F5F9;
  --color-brand: #60A5FA;
  --color-surface: #1E293B;
  --color-border: #334155;
}
// Apply theme before first paint (inline script in <head>)
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);

使用する場合: ほとんどの本番アプリケーションは、ユーザー controlled テーマ切り替えが必要です。データ属性アプローチは現在の業界標準 - Tailwind CSS のダーク モード実装、Radix UI、ほとんどの最新デザイン システムで使用。

パターン 3:クラス ベース テーマ切り替え

データ属性に似ていますが、CSS クラスを使用します。Tailwind の darkMode: 'class' 構成はこのパターンに依存します:

.dark {
  --color-bg: #0F172A;
  --color-text: #F1F5F9;
}

dark クラスを <html> に追加すると、暗いトークン値がアクティブになります。Tailwind はこのクラスが存在する場合、dark: プレフィックス ユーティリティを適用します。

使用する場合: Tailwind CSS class ダーク モード戦略を使用するプロジェクト。


色の CSS 変数:トークン システム

セマンティック ネーミング 対 記述ネーミング

トークン システムの最も重要な決定はネーミング。色値によるネーミング(--blue-500--gray-900)は分離で変数を理解しやすいですが、テーミング不可能 - --blue-500 を紫に変更するとネーム セマンティック破ります。

セマンティック役割によるネーミング(--color-brand--color-text-muted)により値がテーマ全体で変更できるコンポーネント修正のままで:

:root {
  /* ---- Color Primitives (not used directly in components) ---- */
  --blue-500: #3B82F6;
  --blue-700: #1D4ED8;
  --slate-50: #F8FAFC;
  --slate-800: #1E293B;
  --slate-900: #0F172A;

  /* ---- Semantic Tokens (used in components) ---- */
  /* Text */
  --text-primary: var(--slate-800);
  --text-secondary: #64748B;
  --text-disabled: #94A3B8;
  --text-inverse: var(--slate-50);

  /* Surfaces */
  --bg-base: var(--slate-50);
  --bg-elevated: #FFFFFF;
  --bg-sunken: #F1F5F9;

  /* Brand */
  --brand: var(--blue-700);
  --brand-hover: #1E40AF;
  --brand-subtle: #EFF6FF;
  --on-brand: #FFFFFF;

  /* Feedback */
  --error: #DC2626;
  --error-bg: #FEF2F2;
  --success: #16A34A;
  --success-bg: #F0FDF4;
  --warning: #D97706;
  --warning-bg: #FFFBEB;
}

[data-theme="dark"] {
  --text-primary: #F1F5F9;
  --text-secondary: #94A3B8;
  --text-disabled: #475569;
  --text-inverse: #0F172A;

  --bg-base: #0F172A;
  --bg-elevated: #1E293B;
  --bg-sunken: #0D1426;

  --brand: #60A5FA;
  --brand-hover: #93C5FD;
  --brand-subtle: #172554;
  --on-brand: #0F172A;

  --error: #F87171;
  --error-bg: #1C0A0A;
  --success: #4ADE80;
  --success-bg: #052E16;
  --warning: #FCD34D;
  --warning-bg: #1C1000;
}

2 層システム(プリミティブ + セマンティック トークン)は両方の最高を与えます:リファレンス用の罰金色パレット、およびコンポーネント使用のためのセマンティック トークン。

カラー スケール生成

トークン システムを構築する前に、罰金色パレットが必要です。Shade Generator は単一のブランド色から完全な 50-950 スケール生成 - Tailwind CSS で使用される同じスケール パターン。ブランド 16 進コードを入力し、使用の準備が完全なダーク光バリアント セット取得します。

例えば、#2563EB をブランド青として入力することで以下が生成されます:

  • blue-50: #EFF6FF
  • blue-100: #DBEAFE
  • blue-500: #3B82F6
  • blue-700: #1D4ED8
  • blue-900: #1E3A8A
  • blue-950: #172554

これらのプリミティブ値はその後、セマンティック トークンを母集団 、ライト モード色は調和関連の代わりに昏い近似ダーク モード色に確認します。


テーマ状態 React コンテキスト

ThemeContext パターン

ユーザー controlled テーマ切り替え付きアプリケーション、React コンテキスト状態管理層を提供します。コンテキスト保存はアクティブなテーマ名およびトグル関数を公開します:

// contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'light' | 'dark' | 'system';

interface ThemeContextValue {
  theme: Theme;
  resolvedTheme: 'light' | 'dark'; // Actual applied theme (resolves 'system')
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setThemeState] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'system';
    return (localStorage.getItem('theme') as Theme) || 'system';
  });

  const systemPrefersDark = typeof window !== 'undefined'
    ? window.matchMedia('(prefers-color-scheme: dark)').matches
    : false;

  const resolvedTheme: 'light' | 'dark' =
    theme === 'system'
      ? (systemPrefersDark ? 'dark' : 'light')
      : theme;

  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute('data-theme', resolvedTheme);
    localStorage.setItem('theme', theme);
  }, [theme, resolvedTheme]);

  // Listen for system preference changes when theme === 'system'
  useEffect(() => {
    if (theme !== 'system') return;
    const media = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = () => {
      document.documentElement.setAttribute(
        'data-theme',
        media.matches ? 'dark' : 'light'
      );
    };
    media.addEventListener('change', handler);
    return () => media.removeEventListener('change', handler);
  }, [theme]);

  const setTheme = (newTheme: Theme) => {
    setThemeState(newTheme);
  };

  return (
    <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme(): ThemeContextValue {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
  return ctx;
}

ThemeProvider は、アプリケーション(または関連するサブツリー)をラップしおよび useTheme フック、テーマ状態をコンポーネント ツリー内のどこでも公開:

// components/ThemeToggle.tsx
import { useTheme } from '../contexts/ThemeContext';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
      style={{
        background: 'var(--bg-elevated)',
        color: 'var(--text-primary)',
        border: '1px solid var(--color-border)',
        padding: '8px 16px',
        borderRadius: '8px',
        cursor: 'pointer',
      }}
    >
      {theme === 'dark' ? '☀ Light' : '☾ Dark'}
    </button>
  );
}

コンポーネント CSS カスタム プロパティ使用してスタイル - 色を適用するために resolvedTheme を読む必要がありません。コンテキストは現在のテーマ状態を表示またはコントロール UI のためのみ必要です。

テーマの誤ったフラッシュの防止(FOTWT)

サーバー側レンダリング アプリケーション対面フラッシュ誤ったテーマ問題:サーバーはユーザーのテーマ参照を知らずに HTML をレンダリング、ブラウザがその HTML を一時的に表示し、React は水合い、テーマ正確に適用します - 目に見えるフラッシュが発生。

解決策は、ブロッキング インライン スクリプト <head> 内である、localStorage を読みおよび、ページ表示される前にテーマ属性を適用:

<!-- In your <head>, before any CSS links -->
<script>
  (function() {
    try {
      var stored = localStorage.getItem('theme');
      var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      var theme = stored === 'dark' || stored === 'light'
        ? stored
        : (systemDark ? 'dark' : 'light');
      document.documentElement.setAttribute('data-theme', theme);
    } catch(e) {}
  })();
</script>

Next.js で、これは _document.tsx または App Router のルート layout.tsx に行きます。このスクリプトは CSS パースする前に同期的に実行 から、フラッシュはありません。


Tailwind CSS テーマ

Tailwind v3:クラス ベース ダーク モード

Tailwind v3 内では、ダーク モードは darkMode: 'class'tailwind.config.js で設定する必要があります。これにより Tailwind は dark: ユーティリティを .dark クラスが <html> 要素に存在する場合のみ適用します:

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#EFF6FF',
          100: '#DBEAFE',
          500: '#3B82F6',
          600: '#2563EB',
          700: '#1D4ED8',
          900: '#1E3A8A',
          950: '#172554',
        },
      },
    },
  },
};

コンポーネント dark: プレフィックスを使用してダーク モード バリアント:

<div className="bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100">
  <button className="bg-brand-600 dark:bg-brand-500 text-white hover:bg-brand-700 dark:hover:bg-brand-400">
    Primary Action
  </button>
</div>

テーマ切り替えは .dark クラスを document.documentElement に設定します - データ属性パターンと同じアプローチ、クラスの代わりのみを使用。

Tailwind v4:CSS-First 構成

Tailwind v4 は CSS で、設定を完全に移動し、CSS カスタム プロパティをネイティブに使用:

/* styles.css */
@import "tailwindcss";

@theme {
  --color-brand-50: #EFF6FF;
  --color-brand-100: #DBEAFE;
  --color-brand-500: #3B82F6;
  --color-brand-600: #2563EB;
  --color-brand-700: #1D4ED8;
}

Tailwind v4 はこれらのカスタム プロパティからユーティリティ クラスを生成し、JSX でのすぐに利用可能:className="bg-brand-600 text-white"。ダーク モード構成 v4 内では @variant dark を使用:

@variant dark (&:where([data-theme="dark"] *)) {
  /* Tailwind applies dark: utilities based on this selector */
}

このバリアント、祖先が data-theme="dark" を持つ場合、Tailwind に dark: ユーティリティを適用するほう言い、データ属性パターンとネイティブ統合します。


マルチ ブランド テーミング戦略

チャレンジ

複数のクライアントを提供する SaaS プラットフォーム、ホワイト ラベル製品、または複数のプロダクト ブランド全体で共有デザイン システムは、ダーク/ライト バリアント のみではなく、完全に異なるカラー アイデンティティ - それぞれで自分のブランド主要、強調、ステータス色、およびニュートラル を処理する必要があります。

ブランド トークン CSS 変数 オーバーライド

最も保守可能なアプローチは、ブランド不可知なセマンティック トークンをベース スタイルシート内で定義し、ブランドあたりプリミティブ割り当てをオーバーライド:

/* Base theme — same token names for all brands */
:root {
  /* Brand Primitives — overridden per brand */
  --brand-primary-raw: 37 99 235; /* #2563EB in RGB channels */
  --brand-accent-raw: 99 102 241; /* #6366F1 in RGB channels */

  /* Semantic tokens — computed from primitives */
  --brand-primary: rgb(var(--brand-primary-raw));
  --brand-primary-hover: color-mix(in srgb, rgb(var(--brand-primary-raw)) 80%, black);
  --brand-primary-subtle: color-mix(in srgb, rgb(var(--brand-primary-raw)) 10%, white);
}

/* Brand: Acme Corp (blue) */
[data-brand="acme"] {
  --brand-primary-raw: 37 99 235;   /* #2563EB */
  --brand-accent-raw: 16 185 129;   /* #10B981 */
}

/* Brand: Globex Inc (purple) */
[data-brand="globex"] {
  --brand-primary-raw: 124 58 237;  /* #7C3AED */
  --brand-accent-raw: 245 158 11;   /* #F59E0B */
}

/* Brand: Initech (green) */
[data-brand="initech"] {
  --brand-primary-raw: 22 163 74;   /* #16A34A */
  --brand-accent-raw: 239 68 68;    /* #EF4444 */
}

RGB チャネル技術を使用(37 99 235)により不透明度バリアント追加トークンなし:

.overlay {
  background-color: rgba(var(--brand-primary-raw), 0.1);
}

React マルチ ブランド コンテキスト

テーマ コンテキストをブランド処理に拡張:

// contexts/ThemeContext.tsx
type Brand = 'acme' | 'globex' | 'initech' | 'default';
type Theme = 'light' | 'dark';

interface ThemeContextValue {
  theme: Theme;
  brand: Brand;
  setTheme: (theme: Theme) => void;
  setBrand: (brand: Brand) => void;
}

export function ThemeProvider({ initialBrand = 'default', children }: {
  initialBrand?: Brand;
  children: React.ReactNode;
}) {
  const [theme, setTheme] = useState<Theme>('light');
  const [brand, setBrand] = useState<Brand>(initialBrand);

  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute('data-theme', theme);
    root.setAttribute('data-brand', brand);
  }, [theme, brand]);

  return (
    <ThemeContext.Provider value={{ theme, brand, setTheme, setBrand }}>
      {children}
    </ThemeContext.Provider>
  );
}

初期ブランドをルーティングまたは認証レイヤーから設定 - 各クライアントのサブドメインまたはテナント ID がブランド識別子にマップ。

Shade Generator でブランド スケール生成

各ブランドのため、1 つまたは 2 つだけ 16 進値ではなく、完全なカラー スケールが必要です。Shade Generator を使用して各ブランドのプライマリおよびアクセント色のため 50-950 スケールを生成。ブランドのプライマリ 16 進を入力し、ダーク-ライト バリアント の完全な範囲を取得します。

Globex Inc が主要 #7C3AED を持つ場合、shade generator は完全紫スケールを生成します。Initech が #16A34A を持つ場合、完全な緑スケール。これらのスケールはその後、各ブランドのプリミティブ トークンを母集団 ,確認が --brand-primary-subtle(最も軽いティント)および --brand-primary-hover(より暗い押された状態) 各ブランド アイデンティティ 内で調和関連の保持が続きます。


重要なポイント

  • CSS カスタム プロパティが基盤:セマンティック トークン定義(--text-primary--brand-primary)および条件付きで各テーマのクラスがすべてのコンポーネント内では選択肢、値を変更、テーマの。
  • データ属性テーマ切り替えdata-theme="dark")が最も柔軟なパターンです - JavaScript でオーバーライド可能、SSR と互換性がある、prefers-color-scheme メディア クエリと組み合わせ可能。
  • 2 層トークン システム(プリミティブ色スケール + セマンティック役割トークン)により完全なテーマ柔軟性が同時にコンポーネントをクリーン保ち:プリミティブがパレット定義、セマンティックは色の使用方法を定義。
  • Shade Generator を使用してブランド主要から完全な 50-950 色スケールを生成 - これはトロープ トークン システムのための軽いおよび暗いバリアントの完全な範囲を提供。
  • React コンテキスト テーマ状態を管理(アクティブなテーマ名)およびサイド効果(データ属性の設定、localStorage への永続化)。コンポーネントは正しい色を適用するためにコンテキストを読む必要はありません。
  • テーマの誤ったフラッシュを防ぐ ブロッキング インライン スクリプト内 <head> を使用してブラウザがピクセルをレンダリングする前に、localStorage から読みテーマ属性を適用。
  • Tailwind CSS 統合:v3 は darkMode: 'class' を使用して dark: プレフィックス。v4 は CSS-first @theme 構成でネイティブ CSS 変数サポートを使用。
  • マルチ ブランド テーミング は、テーマ システムの上に別のデータ属性(data-brand)をスタック、ブランド あたりプリミティブ トークン値をオーバーライドしながら、セマンティック トークンおよびコンポーネント 不変の保持。

関連カラー

関連ブランド

関連ツール