チュートリアル

動的なカラーシステムのためのCSSカスタムプロパティ

2分で読める

CSSカスタムプロパティ(一般的にCSS変数と呼ばれる)は、最新の保守可能なカラーシステムの基礎です。一度にカラーを定義して、どこでも参照できるため、グローバルテーマの変更は、数百のファイル全体での検索と置換ではなく、1行編集になります。カスケードとJavaScriptと組み合わせると、SassやLessなどのプリプロセッサは単に一致できないダイナミックなテーマ機能をロック解除します。

このチュートリアルは、CSSカスタムプロパティを使用してスクラッチから本番対応のカラーシステムを構築します。基本から、ライト/ダークモード切り替え、コンポーネントレベルのオーバーライド、JavaScriptを使用したランタイムカラー操作まで。

CSSカスタムプロパティ の基本

CSSカスタムプロパティは、名前が2つのダッシュ(--)で始まる任意のプロパティです。他のプロパティのように宣言し、var()関数で読みます。

:root {
  --color-brand: #2563EB;
}

.button {
  background-color: var(--color-brand);
}

:rootセレクタは、html要素と同等ですが、より高い詳細度で、グローバルトークンの従来の場所にしています。ドキュメント内の任意の要素は--color-brandを読み取ることができます。

フォールバック値

var()関数は、変数が定義されていないか無効な場合に使用されるオプションフォールバックを受け入れます。

.card {
  /* --color-secondaryが定義されていない場合、#6B7280にフォールバック */
  color: var(--color-secondary, #6B7280);
}

フォールバックはネストできます。var()への2番目の引数は、それ自体がvar()を使用することが許可されており、これは層化されたデザイントークンシステムに有用です。

カスタムプロパティ対プリプロセッサ変数

SassとLess変数は、コンパイル時に解決され、生成されたCSSに焼き込まれます。コンパイルされると、それは消えます。CSSカスタムプロパティはブラウザに住んでおり、カスケード、継承、JavaScriptに応じます。この区別は、このチュートリアルの続きを有効にします。

カラーテーマアーキテクチャ

よく設計されたトークンシステムには、少なくとも2つのレイヤーがあります。プリミティブトークン (生の値)およびセマンティックトークン (目的駆動型リファレンス)。

プリミティブトークン:完全なパレット

プリミティブトークンは、システムが使用できるすべてのカラーを定義します。ベストプラクティスは、必要な各色の完全なシェードスケールを生成することです。シェードジェネレータを使用して、任意のベースカラーのTailwindスタイル50–950スケールを生成します。

たとえば、ブランドブルー#2563EBから開始します。

:root {
  /* ブルースケール — #2563EBから生成 */
  --blue-50:  #EFF6FF;
  --blue-100: #DBEAFE;
  --blue-200: #BFDBFE;
  --blue-300: #93C5FD;
  --blue-400: #60A5FA;
  --blue-500: #3B82F6;
  --blue-600: #2563EB;
  --blue-700: #1D4ED8;
  --blue-800: #1E40AF;
  --blue-900: #1E3A8A;
  --blue-950: #172554;

  /* ニュートラルスケール */
  --gray-50:  #F9FAFB;
  --gray-100: #F3F4F6;
  --gray-200: #E5E7EB;
  --gray-300: #D1D5DB;
  --gray-400: #9CA3AF;
  --gray-500: #6B7280;
  --gray-600: #4B5563;
  --gray-700: #374151;
  --gray-800: #1F2937;
  --gray-900: #111827;
  --gray-950: #030712;
}

プリミティブトークンはコンポーネントスタイルで直接使用されません。それらは単にセマンティックトークンによって参照されるために存在します。

セマンティックトークン:値上の意図

セマンティックトークンは、生の値に意味を与えます。--blue-600ではなく、コンポーネントは--color-action-primaryを使用しています。この抽象化により、後で--color-action-primaryを緑またはパープルに再割り当てでき、それを使用するすべてのコンポーネントが自動的に更新されます。

:root {
  /* 背景 */
  --color-bg-base:       var(--gray-50);
  --color-bg-surface:    #FFFFFF;
  --color-bg-elevated:   #FFFFFF;
  --color-bg-subtle:     var(--gray-100);

  /* テキスト */
  --color-text-primary:  var(--gray-900);
  --color-text-secondary: var(--gray-600);
  --color-text-disabled: var(--gray-400);
  --color-text-inverse:  #FFFFFF;

  /* ブランド/アクション */
  --color-action-primary:       var(--blue-600);
  --color-action-primary-hover: var(--blue-700);
  --color-action-primary-text:  #FFFFFF;

  /* ボーダー */
  --color-border-base:   var(--gray-200);
  --color-border-strong: var(--gray-400);

  /* フィードバック */
  --color-success: #16A34A;
  --color-warning: #D97706;
  --color-error:   #DC2626;
  --color-info:    var(--blue-600);
}

コンポーネントスタイルは、セマンティックトークンを排他的に参照します。

.button-primary {
  background-color: var(--color-action-primary);
  color:            var(--color-action-primary-text);
  border:           1px solid var(--color-action-primary);
}

.button-primary:hover {
  background-color: var(--color-action-primary-hover);
}

.card {
  background-color: var(--color-bg-surface);
  border:           1px solid var(--color-border-base);
  color:            var(--color-text-primary);
}

この2層アーキテクチャは、システムを柔軟で反ファクタリング可能に保ちます。ブランドカラーが変更された場合、1つのプリミティブトークンを更新し、セマンティックレイヤーはどこでもその変更を伝播します。

ライト/ダークトグルパターン

CSSカスタムプロパティはドキュメント木全体を継承します。これは、特定のスコープでトークンを再定義でき、すべての子孫が新しい値を継承することを意味しています。これは、ライト/ダークテーマを簡潔にするメカニズムです。

アプローチ1:prefers-color-scheme(自動)

最も単純な実装は、ユーザーのオペレーティングシステムの優先度に応答します。

:root {
  /* ライトモード(デフォルト) */
  --color-bg-base:      #FFFFFF;
  --color-bg-surface:   #F9FAFB;
  --color-text-primary: #111827;
  --color-text-secondary: #6B7280;
  --color-border-base:  #E5E7EB;
  --color-action-primary: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    /* ダークモードオーバーライド */
    --color-bg-base:      #111827;
    --color-bg-surface:   #1F2937;
    --color-text-primary: #F9FAFB;
    --color-text-secondary: #9CA3AF;
    --color-border-base:  #374151;
    --color-action-primary: #3B82F6;
  }
}

すべてのコンポーネントは自動的に正しいカラーを取得します。JavaScriptは必要ありません。コンポーネントCSSはまったく変わりません。

アプローチ2:データ属性トグル(手動)

ユーザーコントロール済みテーマ切り替えの場合、一般的なパターンはhtmlまたはbody要素のdata-theme属性です。

:root,
[data-theme="light"] {
  --color-bg-base:      #FFFFFF;
  --color-text-primary: #111827;
  --color-action-primary: #2563EB;
}

[data-theme="dark"] {
  --color-bg-base:      #111827;
  --color-text-primary: #F9FAFB;
  --color-action-primary: #60A5FA;
}

ダークモードは、プライマリアクションカラーに#2563EB(ブルー600)ではなく#60A5FA(ブルー400)を使用しています。ダーク背景では、ブルーのより軽いシェードは許容可能なコントラストを維持しています。コントラストチェッカーを使用して、すべてのセマンティックトークンペア(背景上のテキスト)が両方のモードでWCAG AAを渡すことを確認してください。

小さなJavaScript関数でテーマを切り替えます。

function setTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  localStorage.setItem('color-theme', theme);
}

// ページロード時に保存された環境設定を尊重する
const saved = localStorage.getItem('color-theme');
if (saved) {
  setTheme(saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  setTheme('dark');
}

// トグルボタン
document.getElementById('theme-toggle').addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme');
  setTheme(current === 'dark' ? 'light' : 'dark');
});

非スタイルのコンテンツのフラッシュを防ぐ

テーマの永続化にlocalStorageを使用する場合、<head>にブロッキング列スクリプトを配置してください。スタイルシートの前に、ブラウザが塗料する前に保存されたテーマ属性を適用します。

<head>
  <script>
    const theme = localStorage.getItem('color-theme') ||
      (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
    document.documentElement.setAttribute('data-theme', theme);
  </script>
  <link rel="stylesheet" href="styles.css">
</head>

この小さいインラインスクリプトにより、CSSが解析される前に正しいテーマが適用され、不十分に実装されたダークモードを苦しめる白いフラッシュ、次にダーク遷移を排除します。

コンポーネントレベルのカラーオーバーライド

CSSカスタムプロパティの最も強力な機能の1つは スコープオーバーライド です — 特定のコンポーネントのスコープ内でトークンを再定義して、その子が局所値を継承します。

ユースケース:反転(ダーク)ヒーローセクション

グローバルセマンティックトークン--color-bg-baseが白く設定されており、すべてのテキストが自動的に適応するダークヒーローセクションが必要だとします。

.hero--dark {
  --color-bg-base:        #111827;
  --color-text-primary:   #F9FAFB;
  --color-text-secondary: #9CA3AF;
  --color-border-base:    #374151;

  background-color: var(--color-bg-base);
  color: var(--color-text-primary);
}

/* 変更が必要ありません — .hero__titleは上書きされた値を継承します */
.hero__title {
  color: var(--color-text-primary);
}

.hero__subtitle {
  color: var(--color-text-secondary);
}

.hero--dark修飾子は局所的にトークンを再定義します。.hero--dark内のそれらのトークンを使用するすべての要素は、自動的にダーク値を継承し、hero__title--darkなどのクラス名の重複なし。

ユースケース:ブランド化されたアラートバリアント

コンポーネントレベルのオーバーライドはセマンティックバリアントに理想的です。

.alert {
  background-color: var(--alert-bg, var(--color-bg-subtle));
  color:            var(--alert-text, var(--color-text-primary));
  border-left:      4px solid var(--alert-accent, var(--color-border-strong));
  padding:          1rem 1.25rem;
  border-radius:    0.375rem;
}

.alert--success {
  --alert-bg:     #F0FDF4;
  --alert-text:   #14532D;
  --alert-accent: #16A34A;
}

.alert--error {
  --alert-bg:     #FEF2F2;
  --alert-text:   #7F1D1D;
  --alert-accent: #DC2626;
}

.alert--warning {
  --alert-bg:     #FFFBEB;
  --alert-text:   #78350F;
  --alert-accent: #D97706;
}

.alertベースコンポーネントは、プライベートコンポーネントレベル変数(--alert-bg--alert-text--alert-accent)をグローバルトークンへのフォールバックで参照します。バリアント修飾子はそれらのプライベート変数を設定します。このパターンはベースコンポーネントのCSSをクリーンに保ち、新しいバリアントの追加を簡潔にします。

JavaScriptを使用したランタイムカラー変更

CSSカスタムプロパティはブラウザに住んでいるため、JavaScriptはいつでもそれらを読み書きできます。ユーザー入力、データ、またはアプリケーションの状態に応答するダイナミックなカラーシステムを有効にします。

カスタムプロパティの読み取りと書き込み

const root = document.documentElement;

// カスタムプロパティを読む
const brandColor = getComputedStyle(root).getPropertyValue('--color-action-primary').trim();
console.log(brandColor); // '#2563EB'

// カスタムプロパティを設定
root.style.setProperty('--color-action-primary', '#7C3AED');

// オーバーライドを削除(スタイルシート値に戻す)
root.style.removeProperty('--color-action-primary');

ユースケース:ユーザーカスタマイズ可能なブランドカラー

各々のテナントがブランドカラーを設定できるSaasDashboardを構築していると想定してください。APIからブランドカラーを受け取り、ランタイムで適用します。

async function applyTenantTheme(brandHex) {
  // brandHex: '#7C3AED' (パープル)
  const root = document.documentElement;

  // プライマリアクションカラーを設定
  root.style.setProperty('--color-action-primary', brandHex);

  // ホバーバリアントを導出(サーバー側で計算されるか、小さなライブラリ経由)
  const hoverHex = darkenColor(brandHex, 0.1);
  root.style.setProperty('--color-action-primary-hover', hoverHex);
}

カラーコンバータは、シェード導出ロジックを構築するときに有用なHEXコードに対応するOKLCHまたはHSL値を計算するのを支援できます。たとえば、#7C3AEDはOKLCHで概ねoklch(0.52 0.23 295)です。CおよびHを定数に保つ間、Lコンポーネントを調整してより軽くより暗いバリアントを導出できます。

ユースケース:リアルタイムカラーテーマピッカー

<input type="color" id="brand-picker" value="#2563EB">
document.getElementById('brand-picker').addEventListener('input', (e) => {
  document.documentElement.style.setProperty('--color-action-primary', e.target.value);
});

これは12行のコードの正規のライブテーマエディタです。ビルドステップなし、ライブラリなし、再レンダーサイクルなし。ブラウザは、ユーザーがカラーピッカーをドラッグするため、--color-action-primaryを使用するすべての要素を即座に更新します。

カスタムプロパティをアニメーション化する

カスタムプロパティは、プロパティの型と初期値の宣言を許可するHoudini APIである@propertyを使用して補間することもできます。

@property --gradient-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.animated-border {
  --gradient-angle: 0deg;
  background: conic-gradient(
    from var(--gradient-angle),
    #2563EB, #7C3AED, #EC4899, #2563EB
  );
  animation: rotate 4s linear infinite;
}

@keyframes rotate {
  to { --gradient-angle: 360deg; }
}

@propertyなしでは、ブラウザは--gradient-angleが角度であることを知らず、それを補間できません。それが一緒にあれば、アニメーションは滑らかに実行されます。

主要なポイント

  • CSSカスタムプロパティはライブ値です、コンパイル時置換ではなく。カスケード、継承、JavaScriptはすべてに適用されます。これは、プリプロセッサ変数システムが一致できない機能を提供します。
  • 2層トークンアーキテクチャを使用します :プリミティブトークン(生のカラー値)とセマンティックトークン(目的駆動型リファレンス)。コンポーネントは、セマンティックトークンのみを参照します。これにより、大規模なリファクタリングは1行の変更になります。
  • ライト/ダークモードは単なるトークンの再定義です :自動切り替えの場合は@media (prefers-color-scheme: dark)を使用するか、手動制御の場合はdata-theme属性を使用します。コンポーネントは変異固有のCSSが必要ありません。
  • コンポーネントレベルのオーバーライド により、局所スコープでトークンを再定義することで、反転されたセクションとセマンティックバリアントを作成できます。追加のクラス名または重複したCSS は必要ありません。
  • JavaScriptはランタイムでカスタムプロパティを読み書きできます、ライブテーマエディタ、テナント固有のブランディング、データ駆動型のカラー変更を有効にします。
  • コントラストチェッカーを使用して、ライトおよびダークモード両方でアクセス可能なコントラストを常に確認します。ダークモードの青と緑は、適切なコントラスト比を維持するために、ライトモードの対応物よりも軽くなる必要があることが多いです。
  • シェードジェネレータを使用してプリミティブトークンパレットの完全で均等に間隔を開けたカラースケールを生成し、カラーコンバータを使用して、導出ロジックを構築するときにHEX、RGB、HSL、OKLCHの間で翻訳します。

関連カラー

関連ブランド

関連ツール