CSS変数(カスタムプロパティ)完全ガイド|テーマカラーの管理方法とダークモード対応の実装を一から解説


はじめに

前回の記事ではレスポンシブ対応の実践的なコツをHTML・CSS両方の観点からまとめました。今回はCSS変数(カスタムプロパティ)を使ったテーマカラーの管理方法とダークモード対応の実装を解説します。CSS変数はサイト全体の色・フォント・余白を一箇所で管理できる仕組みで、デザインの変更をファイル全体に一括反映できます。ダークモード対応の実装まで合わせて解説します。


1. CSS変数(カスタムプロパティ)とは

1.1 CSS変数の概要

**CSS変数(カスタムプロパティ)**とはCSS内に独自の変数を定義して再利用できる仕組みです。-- から始まる名前で定義して var() 関数で使います。

/* 変数の定義 */
:root {
    --color-primary: #1A5FC0;
}

/* 変数の使用 */
.btn {
    background-color: var(--color-primary);
}

1.2 Sassの変数との違い

CSSにはもともとSass(CSSプリプロセッサ)の変数($color: #1A5FC0)がよく使われてきました。CSS変数との主な違いは以下の通りです。

比較項目CSS変数Sass変数
動的な変更JavaScriptやメディアクエリで実行時に変更できるビルド時に固定される
スコープ要素ごとにスコープを持てる(カスケード)グローバルまたはブロックスコープのみ
ブラウザサポート現代ブラウザで完全対応ビルドツールが必要
DevToolsでの確認開発者ツールでリアルタイムに値を確認できるコンパイル後のCSSしか確認できない

💡 新しいプロジェクトではCSS変数を優先的に使いましょう: ビルドツールが不要で学習コストも低く、JavaScriptとの連携もシンプルです。


2. テーマカラーをCSS変数で管理する

2.1 カラーシステムの設計

CSS変数でカラーシステムを設計するとき、「原始値」と「セマンティックな名前」の2層構造にすると変更に強いシステムになります。

:root {
    /* ===== 第1層:原始値(プリミティブ) ===== */
    /* 実際の色の値を定義する */

    /* ブルー系 */
    --blue-100: #dbeafe;
    --blue-300: #93c5fd;
    --blue-500: #3b82f6;
    --blue-600: #1A5FC0;
    --blue-800: #0d3a7a;
    --blue-900: #1e3a5f;

    /* グレー系 */
    --gray-50:  #f9fafb;
    --gray-100: #f3f4f6;
    --gray-200: #e5e7eb;
    --gray-400: #9ca3af;
    --gray-600: #4b5563;
    --gray-800: #1f2937;
    --gray-900: #111827;

    /* アクセント系 */
    --green-500: #22c55e;
    --red-500:   #ef4444;
    --yellow-500: #eab308;

    /* ===== 第2層:セマンティック(意味のある名前) ===== */
    /* 用途に応じた名前で参照する */

    /* 背景色 */
    --color-bg:          var(--gray-50);
    --color-bg-sub:      var(--gray-100);
    --color-bg-muted:    var(--gray-200);

    /* テキスト色 */
    --color-text:        var(--gray-900);
    --color-text-sub:    var(--gray-600);
    --color-text-muted:  var(--gray-400);
    --color-text-inverse: #ffffff;

    /* ブランドカラー */
    --color-primary:       var(--blue-600);
    --color-primary-hover: var(--blue-800);
    --color-primary-light: var(--blue-100);

    /* ステータスカラー */
    --color-success: var(--green-500);
    --color-error:   var(--red-500);
    --color-warning: var(--yellow-500);

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

    /* シャドウ */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
    --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.12);
}

💡 セマンティックな名前を使うとダークモード対応が簡単になります: --color-bg という名前なら、ダークモードのときに :root の値を書き換えるだけで全体に反映されます。--gray-50 のような具体的な色名を直接使うと、ダークモードで個別に上書きする必要が生じて管理が煩雑になります。

2.2 フォントと余白もCSS変数で管理する

:root {
    /* ===== タイポグラフィ ===== */
    --font-sans:  'Noto Sans JP', 'Inter', sans-serif;
    --font-mono:  'Fira Code', 'Courier New', monospace;

    --text-xs:   0.75rem;   /* 12px */
    --text-sm:   0.875rem;  /* 14px */
    --text-base: 1rem;      /* 16px */
    --text-lg:   1.125rem;  /* 18px */
    --text-xl:   1.25rem;   /* 20px */
    --text-2xl:  1.5rem;    /* 24px */
    --text-3xl:  1.875rem;  /* 30px */
    --text-4xl:  2.25rem;   /* 36px */
    --text-5xl:  3rem;      /* 48px */

    --leading-tight:  1.25;
    --leading-normal: 1.6;
    --leading-loose:  1.9;

    --font-normal: 400;
    --font-medium: 500;
    --font-bold:   700;

    /* ===== 余白(スペーシング) ===== */
    --space-1:  0.25rem;  /* 4px */
    --space-2:  0.5rem;   /* 8px */
    --space-3:  0.75rem;  /* 12px */
    --space-4:  1rem;     /* 16px */
    --space-6:  1.5rem;   /* 24px */
    --space-8:  2rem;     /* 32px */
    --space-10: 2.5rem;   /* 40px */
    --space-12: 3rem;     /* 48px */
    --space-16: 4rem;     /* 64px */
    --space-20: 5rem;     /* 80px */
    --space-24: 6rem;     /* 96px */

    /* ===== 角丸 ===== */
    --radius-sm:   4px;
    --radius-md:   8px;
    --radius-lg:   12px;
    --radius-xl:   16px;
    --radius-full: 9999px;

    /* ===== z-index ===== */
    --z-dropdown: 20;
    --z-header:   30;
    --z-modal:   100;
    --z-toast:   200;

    /* ===== トランジション ===== */
    --transition-fast:   0.15s ease;
    --transition-base:   0.25s ease;
    --transition-slow:   0.4s ease;
}

2.3 実際の使い方

/* CSS変数を使ったコンポーネントの記述 */

body {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    color: var(--color-text);
    background-color: var(--color-bg);
}

/* ボタン */
.btn-primary {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-8);
    background-color: var(--color-primary);
    color: var(--color-text-inverse);
    font-weight: var(--font-medium);
    font-size: var(--text-base);
    border-radius: var(--radius-md);
    border: none;
    cursor: pointer;
    transition: background-color var(--transition-base),
                transform var(--transition-fast),
                box-shadow var(--transition-base);
}

.btn-primary:hover {
    background-color: var(--color-primary-hover);
    box-shadow: var(--shadow-md);
    transform: translateY(-1px);
}

/* カード */
.card {
    background-color: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
    box-shadow: var(--shadow-sm);
    transition: box-shadow var(--transition-base),
                border-color var(--transition-base);
}

.card:hover {
    box-shadow: var(--shadow-md);
    border-color: var(--color-border-hover);
}

3. ダークモードを実装する

3.1 prefers-color-schemeメディアクエリ

OSの設定(ダークモード・ライトモード)を CSS のメディアクエリで検出できます。

/* ライトモード(デフォルト) */
:root {
    --color-bg:       #ffffff;
    --color-bg-sub:   #f9fafb;
    --color-text:     #111827;
    --color-text-sub: #4b5563;
    --color-primary:  #1A5FC0;
    --color-border:   #e5e7eb;
    --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
}

/* ダークモード */
@media (prefers-color-scheme: dark) {
    :root {
        --color-bg:       #0f172a;
        --color-bg-sub:   #1e293b;
        --color-text:     #f1f5f9;
        --color-text-sub: #94a3b8;
        --color-primary:  #60a5fa;
        --color-border:   #334155;
        --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
    }
}

この設定だけでボタン・カード・テキストなどCSS変数を使っているすべての要素が自動的にダークモードに切り替わります。

3.2 手動でダークモードを切り替える

OSの設定に依存するだけでなく、ボタンで手動切り替えもできるようにするとユーザー体験が上がります。

HTML:

<!-- ダークモード切り替えボタン -->
<button class="theme-toggle" id="theme-toggle" aria-label="ダークモードを切り替える">
    <span class="theme-icon theme-icon--light" aria-hidden="true">☀️</span>
    <span class="theme-icon theme-icon--dark"  aria-hidden="true">🌙</span>
</button>

CSS:

/* data-theme属性でテーマを切り替える */

/* ライトモード(デフォルト) */
:root,
[data-theme="light"] {
    --color-bg:       #ffffff;
    --color-bg-sub:   #f9fafb;
    --color-text:     #111827;
    --color-text-sub: #4b5563;
    --color-primary:  #1A5FC0;
    --color-border:   #e5e7eb;
}

/* ダークモード */
[data-theme="dark"] {
    --color-bg:       #0f172a;
    --color-bg-sub:   #1e293b;
    --color-text:     #f1f5f9;
    --color-text-sub: #94a3b8;
    --color-primary:  #60a5fa;
    --color-border:   #334155;
}

/* OSの設定がダークモードのときはデフォルトでダークにする */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --color-bg:       #0f172a;
        --color-bg-sub:   #1e293b;
        --color-text:     #f1f5f9;
        --color-text-sub: #94a3b8;
        --color-primary:  #60a5fa;
        --color-border:   #334155;
    }
}

/* ボタンのアイコン切り替え */
[data-theme="dark"] .theme-icon--light { display: none; }
[data-theme="dark"] .theme-icon--dark  { display: inline; }

.theme-icon--dark { display: none; }
.theme-icon--light { display: inline; }

JavaScript:

const toggle    = document.getElementById( 'theme-toggle' );
const root      = document.documentElement;
const STORAGE_KEY = 'theme';

// 保存済みのテーマを読み込む
const savedTheme = localStorage.getItem( STORAGE_KEY );
if ( savedTheme ) {
    root.setAttribute( 'data-theme', savedTheme );
}

// ボタンでテーマを切り替える
toggle.addEventListener( 'click', () => {
    const current = root.getAttribute( 'data-theme' );
    const next    = current === 'dark' ? 'light' : 'dark';

    root.setAttribute( 'data-theme', next );
    localStorage.setItem( STORAGE_KEY, next );

    // aria-labelも更新する
    toggle.setAttribute(
        'aria-label',
        next === 'dark' ? 'ライトモードに切り替える' : 'ダークモードに切り替える'
    );
} );

3.3 テーマ切り替え時のちらつきを防ぐ

ページ読み込み時に一瞬ライトモードが表示されてからダークモードに切り替わる「ちらつき(FOUC)」を防ぎます。

<!-- <head>の中・CSSより前に記述する -->
<script>
    (function() {
        const saved  = localStorage.getItem( 'theme' );
        const prefer = window.matchMedia( '(prefers-color-scheme: dark)' ).matches
            ? 'dark'
            : 'light';
        const theme  = saved || prefer;
        document.documentElement.setAttribute( 'data-theme', theme );
    })();
</script>

⚠️ このスクリプトはCSSより前に読み込む必要があります: <head> の中・<link rel="stylesheet"> より前に配置することで、スタイルが適用される前にテーマが設定されます。


4. CSS変数をJavaScriptから操作する

4.1 JavaScriptでCSS変数を取得・変更する

// CSS変数の値を取得する
const root = document.documentElement;
const primaryColor = getComputedStyle( root )
    .getPropertyValue( '--color-primary' )
    .trim();

console.log( primaryColor ); // "#1A5FC0"

// CSS変数の値を変更する
root.style.setProperty( '--color-primary', '#8E44AD' );

// 特定の要素のCSS変数を変更する
const card = document.querySelector( '.card' );
card.style.setProperty( '--card-bg', '#f0f4ff' );

4.2 コンポーネントのカラーをCSS変数で渡す

<!-- style属性でCSS変数を渡す -->
<div class="progress-bar" style="--progress: 75%;">
    <div class="progress-fill"></div>
</div>

<div class="avatar" style="--avatar-color: #8E44AD;">
    山田
</div>
/* CSS変数をコンポーネントのスタイルに使う */
.progress-bar {
    height: 8px;
    background-color: var(--color-border);
    border-radius: var(--radius-full);
    overflow: hidden;
}

.progress-fill {
    height: 100%;
    width: var(--progress, 0%);  /* 第2引数はフォールバック値 */
    background-color: var(--color-primary);
    border-radius: var(--radius-full);
    transition: width var(--transition-slow);
}

.avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background-color: var(--avatar-color, var(--color-primary));
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-sm);
    font-weight: var(--font-bold);
}

💡 var() の第2引数はフォールバック値です: var(--progress, 0%) のように書くと変数が未定義の場合に 0% が使われます。コンポーネントのデフォルト値として活用しましょう。


5. ダークモードで注意すべきポイント

5.1 画像の扱い

/* ダークモードで画像の輝度を少し下げる */
@media (prefers-color-scheme: dark) {
    img:not([src*=".svg"]) {
        filter: brightness(0.9);
    }
}

/* または data-theme を使う場合 */
[data-theme="dark"] img:not([src*=".svg"]) {
    filter: brightness(0.85);
}

5.2 ボーダーとシャドウの調整

/* ダークモードではシャドウが見えにくいのでボーダーで代用する */
:root {
    --shadow-card: 0 4px 12px rgba(0, 0, 0, 0.1);
    --border-card: 1px solid transparent;
}

[data-theme="dark"] {
    --shadow-card: none;
    --border-card: 1px solid var(--color-border);
}

.card {
    box-shadow: var(--shadow-card);
    border:     var(--border-card);
}

5.3 コントラスト比の確認

ダークモードでもWCAGの基準(テキストと背景が4.5:1以上)を満たしているか確認します。

組み合わせコントラスト比評価
#f1f5f9 テキスト / #0f172a 背景約15:1✅ 非常に良好
#94a3b8 サブテキスト / #0f172a 背景約5:1✅ AA基準クリア
#60a5fa リンク / #0f172a 背景約7:1✅ 非常に良好

6. 実装チェックリスト

原始値とセマンティックな名前の2層構造でCSS変数を設計しているか

色だけでなくフォント・余白・角丸・z-indexもCSS変数で管理しているか

prefers-color-scheme: dark でOSのダークモード設定に対応しているか

data-theme 属性を使った手動切り替えが実装されているか

ユーザーのテーマ設定を localStorage に保存しているか

ページ読み込み時のちらつき防止スクリプトを <head> の先頭に入れているか

ダークモードでも画像が適切な明るさで表示されているか

ダークモードのコントラスト比が4.5:1以上か確認したか

var() のフォールバック値を適切に設定しているか

Chrome DevToolsでライト・ダーク両方の表示を確認したか


まとめ

今回はCSS変数を使ったテーマカラーの管理方法とダークモード対応の実装を解説しました。ポイントをまとめると:

  • CSS変数は原始値(--blue-600)とセマンティックな名前(--color-primary)の2層構造にすると変更に強い
  • 色だけでなくフォント・余白・角丸・z-index・トランジションもCSS変数で一元管理する
  • prefers-color-scheme: dark だけでOSの設定に自動対応できる
  • 手動切り替えには data-theme 属性と localStorage の組み合わせが使いやすい
  • ページ読み込み時のちらつき防止スクリプトは <head> の最初に配置する
  • ダークモードでは画像の輝度を少し下げ・シャドウをボーダーに置き換えると自然に見える
  • コンポーネントに style 属性でCSS変数を渡すと再利用性の高い実装ができる

次の記事では、PHPを使った「お問い合わせフォーム(送信処理)」の仕組みについて解説します。お楽しみに!

コメント

タイトルとURLをコピーしました