CSS Custom Properties実践ガイド ―
デザイントークンとして使いこなす
CSS変数(Custom Properties)を「ただの色の定数置き場」だと思っていたら、だいぶもったいない使い方をしている。スコープ、継承、JavaScriptとの連携、そしてダークモードの実装まで、現場で本当に使えるCSS変数の全体像を整理しよう。

CSS変数とSASSの変数は「別物」だと理解する
CSSを書いてきた人なら、SASSやLessの変数を使ったことがあるはずだ。$primary-color: #059669;というアレ。でもCSS Custom Propertiesは、見た目は似ていても根本的に別物だ。SASSの変数はビルド時に展開される「静的な置換」なのに対して、CSS変数はブラウザが実行時に評価する「ライブな値」だ。
この違いは実用上とても大きい。CSS変数はDOMに生きているので、JavaScriptから変更できる。メディアクエリの中で上書きできる。子要素が親から継承する。ユーザーのアクションに応じてリアルタイムに変化させることができる。SASSの変数でできないことが全部できる。
// SASS: コンパイル後は値が直接埋め込まれる
$primary: #059669;
$spacing-md: 16px;
.button {
background: $primary; // → background: #059669; に置換
padding: $spacing-md; // → padding: 16px; に置換
}
// JavaScriptから後から変更する方法がない
/* CSS: ブラウザが実行時に評価するライブな値 */
:root {
--color-primary: #059669;
--spacing-md: 16px;
}
.button {
background: var(--color-primary);
padding: var(--spacing-md);
}
/* JavaScriptから変更可能 */
/* document.documentElement.style.setProperty('--color-primary', '#0891b2') */
スコープと継承を理解する ― これがCSS変数の本質
CSS変数の最も大事な特性は「スコープがある」ことだ。:rootに定義すればグローバルに使えるが、任意のセレクタに定義すればそのスコープ内だけで有効になる。しかも子要素に継承される。これを使いこなすと、コンポーネントレベルでのテーマ管理ができるようになる。
/* グローバル定義 */
:root {
--color-accent: #059669;
--card-bg: #ffffff;
--card-border: #e5e7eb;
}
/* .card コンポーネントのスコープで上書き */
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
padding: 20px;
}
/* バリアント:.card--featured スコープだけ変数を上書き */
.card--featured {
--card-bg: #ecfdf5;
--card-border: #6ee7b7;
/* .card のスタイルは一切書き換えない。変数だけ変える。 */
}
/* .card--danger バリアント */
.card--danger {
--card-bg: #fef2f2;
--card-border: #fca5a5;
}
このパターンの何がうれしいかというと、.cardのスタイル定義を一切コピーしなくても、見た目を変えられること。バリアントは「変数を上書きする」だけでいい。デザインの変更は変数の値を変えるだけで済む。コンポーネント側のロジックはそのまま。これがCSS変数を使ったコンポーネント設計の基本形だ。
通常カード
おすすめカード
警告カード
情報カード
ダークモードをCSS変数でシンプルに実装する
CSS変数はダークモードの実装と相性抜群だ。旧来のやり方は「dark テーマ用に全プロパティを上書きする」という力技だったが、変数を使えば「変数の値を切り替えるだけ」でいい。
/* ライトモード(デフォルト) */
:root {
--bg-base: #ffffff;
--bg-surface: #f9fafb;
--text-primary: #111827;
--text-secondary: #6b7280;
--border: #e5e7eb;
--accent: #059669;
}
/* ダークモード:変数の値だけを入れ替える */
@media (prefers-color-scheme: dark) {
:root {
--bg-base: #0f172a;
--bg-surface: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--border: #334155;
--accent: #34d399;
}
}
/* コンポーネント側:var()で参照するだけ。ダーク対応は自動。 */
body {
background: var(--bg-base);
color: var(--text-primary);
}
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
}
これにより、コンポーネントのスタイル定義は一切変えずにダークモードが完成する。新しいコンポーネントを追加するときも、変数を使って書いておけば自動でダーク対応になる。「ダークモードのことを考えてスタイリングする」から「変数を正しく使ってスタイリングすれば自動でダーク対応」という状態に変わる。
手動トグルも簡単に追加できる
ユーザーが手動でテーマを切り替えられるようにしたい場合も、CSS変数なら数行で対応できる。
/* CSS: data-theme属性でテーマ切り替え */
[data-theme="dark"] {
--bg-base: #0f172a;
--text-primary: #f1f5f9;
/* ... */
}
// JavaScript: ボタンクリックでテーマ切り替え
const btn = document.querySelector('#theme-toggle');
btn.addEventListener('click', () => {
const html = document.documentElement;
const isDark = html.getAttribute('data-theme') === 'dark';
html.setAttribute('data-theme', isDark ? 'light' : 'dark');
});
変数の値を切り替えるだけで、テーマが即座に変わります。
デザイントークンとしての設計 ― 命名規則が全て
CSS変数の効果を最大限に引き出すには、命名規則の設計が鍵になる。よくある失敗が「色の値をそのまま変数名にする」パターン。
:root {
--green: #059669;
--dark-green: #047857;
--light-green: #6ee7b7;
}
/* 問題:ダークモードにすると --green が明るくなることがある。
変数名と値が乖離して混乱を招く。 */
:root {
/* プリミティブトークン(パレット。直接使わない) */
--palette-green-500: #059669;
--palette-green-700: #047857;
--palette-green-200: #6ee7b7;
/* セマンティックトークン(役割ベース。これをコンポーネントで使う) */
--color-accent: var(--palette-green-500);
--color-accent-hover: var(--palette-green-700);
--color-accent-subtle: var(--palette-green-200);
/* スペーシングトークン */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
}
「プリミティブトークン」と「セマンティックトークン」の二層構造がミソだ。プリミティブトークンはカラーパレットそのもの(直接コンポーネントには使わない)、セマンティックトークンは「アクセントカラー」「ホバー時のカラー」という役割を持つ変数で、コンポーネントはこちらだけを参照する。ダークモードの切り替えはセマンティックトークンの値を差し替えるだけで完了する。
JavaScriptとの連携 ― CSSとJSが協調する世界
CSS変数はJavaScriptから読み書きできる。これを使うと、アニメーションのタイミング制御、インタラクションに連動したスタイル変更など、CSSとJSを綺麗に分離しながら連携させる設計が可能になる。
// 変数の値を取得
const value = getComputedStyle(document.documentElement)
.getPropertyValue('--color-accent').trim();
// → "#059669"
// 変数の値を設定(グローバル)
document.documentElement.style.setProperty(
'--color-accent', '#2563eb'
);
// 特定の要素スコープで変数を設定
const card = document.querySelector('.card');
card.style.setProperty('--card-bg', '#eff6ff');
まとめ
- CSS Custom PropertiesはSASSの変数と違い、ブラウザが実行時に評価するライブな値であり、JavaScriptから変更でき、DOMの継承に従って動作する。
- スコープを活用すると「変数を上書きするだけでバリアントを作る」というコンポーネント設計が可能になり、コードの重複を大幅に減らせる。
- ダークモードは:rootの変数値をメディアクエリやdata-theme属性で切り替えるだけで実装でき、コンポーネント側は何も変更しなくてよい。
- 命名規則は「プリミティブトークン(パレット)」と「セマンティックトークン(役割)」の二層構造にすると、保守性が飛躍的に上がる。
- JavaScriptとの連携には getPropertyValue() と setProperty() を使い、スタイルのロジックはCSSに、動的な値の変更はJSに、と責務を分離できる。
- プロジェクト初期に命名規則を設計しておくことが最も重要で、後から変えようとすると全ファイルの書き換えが発生する。