Back

CSS変数を使ったテーマスイッチャーの作成

CSS変数を使ったテーマスイッチャーの作成

スタイルシート全体に散らばったハードコードされた色の値は、テーマ切り替えを困難にします。1つの色を変更するだけで、数十もの宣言を探し回ることになるでしょう。CSS変数はこの問題をきれいに解決します — デザイントークンを一度定義しておけば、ルート要素の単一の属性を変更するだけでテーマを切り替えられます。

この記事では、メンテナンスしやすいテーマスイッチャーの構築方法を解説します。システム設定を自動的に尊重し、ユーザーの手動選択に応答し、その選択をセッション間で保持するものを構築していきます。

主なポイント

  • CSSカスタムプロパティは動的でランタイムに更新されるため、テーマ用のデザイントークンとして理想的です。
  • ルート要素の data-theme 属性は、任意の数のテーマを切り替えるためのクリーンでスケーラブルな方法を提供します。
  • prefers-color-scheme メディアクエリがシステムのデフォルトを処理し、JavaScriptと localStorage がユーザーの手動選択を保持します。
  • <head> 内の小さなインラインスクリプトが、ページ読み込み時の誤ったテーマの一瞬の表示(フラッシュ)を防ぎます。
  • color-scheme プロパティを含めることで、ネイティブのブラウザUIがアクティブなテーマに合わせて適応します。

CSS変数がテーマ化の正しい基盤である理由

CSSカスタムプロパティは動的です。静的な値にコンパイルされるSass変数とは異なり、CSS変数はブラウザ内に存在し、ランタイムで更新できます — メディアクエリ、JavaScript、あるいは状態を変更する親要素によって。

これにより、デザイントークンとして理想的になります。サーフェス色、テキスト色、アクセント色を :root 上の変数として定義し、スタイルシート全体でそれらの変数を参照します。テーマ切り替えは、スタイルシートの差し替えではなく、単一のDOM変更で済むようになります。

テーマトークンのセットアップ

まずライトテーマをデフォルトとして定義し、次に data-theme 属性セレクタを使ってダークバリアントを宣言します:

:root {
  color-scheme: light;
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-primary: #302ae6;
  --color-surface: #f4f4f4;
}

[data-theme="dark"] {
  color-scheme: dark;
  --color-bg: #161625;
  --color-text: #e1e1ff;
  --color-primary: #9a97f3;
  --color-surface: #1e1e30;
}

color-scheme プロパティは含めておく価値があります。これにより、ブラウザにどのモードがアクティブかを伝え、ネイティブのUI要素 — スクロールバー、フォーム入力、フォーカスリングなど — がテーマに合わせて自動的に適応します。

これで、スタイルシート全体でこれらのトークンを使用できます:

body {
  background-color: var(--color-bg);
  color: var(--color-text);
}

prefers-color-scheme でシステム設定を尊重する

システム設定と手動テーマ切り替えは、異なる問題を解決します。prefers-color-scheme は自動的なケース — OSをダークモードに設定していてウェブサイトもそれに従うことを期待しているユーザー — を処理します:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --color-bg: #161625;
    --color-text: #e1e1ff;
    --color-primary: #9a97f3;
    --color-surface: #1e1e30;
  }
}

:not([data-theme="light"]) のガードにより、ユーザーの明示的な選択がシステムのデフォルトをオーバーライドすることが保証されます。これがないと、メディアクエリが手動選択を上書きしてしまいます。

light-dark() についての注記

light-dark() CSS関数を使うと、両方のテーマ値をインラインで宣言できます:

:root {
  color-scheme: light dark;
  --color-bg: light-dark(#ffffff, #161625);
}

シンプルなトークンセットには洗練された方法であり、2つのテーマだけが必要な場合にはうまく機能します。3つ以上のテーマがあるシステムや、色以外にも値が変化するトークンの場合は、属性セレクタによるアプローチの方がより多くの制御を提供します。

light-dark() は、関数が正しく解決されるために color-scheme が両方の値で設定されている必要があることに注意してください。モダンブラウザのサポートは強いものの、まだ比較的最近のものなので(Chrome 123+, Safari 17.5+, Firefox 120+)、これに依存する前にオーディエンスの互換性を確認してください。

手動制御の永続化のためのJavaScript

システム設定の検出はCSSのみで可能ですが、ユーザーの手動選択を記憶するにはJavaScriptが必要です。以下に最小限の、プロダクションで使える実装を示します:

const STORAGE_KEY = 'theme-preference';
const root = document.documentElement;

function getSystemTheme() {
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

function getSavedTheme() {
  try {
    return localStorage.getItem(STORAGE_KEY);
  } catch {
    return null;
  }
}

function applyTheme(theme) {
  root.setAttribute('data-theme', theme);

  try {
    localStorage.setItem(STORAGE_KEY, theme);
  } catch {
    // Storage unavailable — theme still applies for this session
  }
}

// On load: respect saved preference, fall back to system
applyTheme(getSavedTheme() || getSystemTheme());

// Wire up your toggle button
document.getElementById('theme-toggle').addEventListener('click', () => {
  const current = root.getAttribute('data-theme');
  applyTheme(current === 'dark' ? 'light' : 'dark');
});

// Sync with OS changes when no manual preference is set
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  if (!getSavedTheme()) {
    root.setAttribute('data-theme', e.matches ? 'dark' : 'light');
  }
});

localStorage の周りの try/catch ブロックは、プライベートブラウジングモードやストレージの容量エラーを適切に処理します — 保存できなくても、テーマは現在のセッションには適用されます。

誤ったテーマのフラッシュを防ぐ

JavaScriptがページのレンダリング後に読み込まれると、ユーザーは正しいテーマが適用される前に一瞬デフォルトのテーマを目にします。これを <head> 内の小さなブロッキング・インラインスクリプトで修正します:

<script>
  try {
    const saved = localStorage.getItem('theme-preference');

    if (saved) {
      document.documentElement.setAttribute('data-theme', saved);
    } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  } catch {
    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  }
</script>

これはコンテンツがレンダリングされる前に同期的に実行され、フラッシュを排除します。CSSが最初に適用される時点で属性が存在するよう、<head> 内のできるだけ早い位置、スタイルシートよりも前に配置してください。

スケールするパターン

CSS変数は、デザイントークンとコンポーネントスタイルの間にクリーンな分離をもたらします。コンポーネントは変数を参照し、テーマは変数を定義します。3つ目のテーマ — ハイコントラスト、ブランドバリアント、シーズナルパレット — を追加することは、CSSに1つの新しい属性セレクタブロックを追加し、スイッチャーロジックに1つの新しいオプションを追加することを意味します。それ以外は何も変わりません。

結論

ここでの真の価値は、単なるダークモードではありません — それはデザインシステムが成長してもメンテナンス可能であり続けるテーマ化アーキテクチャです。CSS変数をデザイントークンとして扱い、システムとユーザー両方の設定を尊重し、エッジケース(ストレージの失敗、レンダリングのフラッシュ、OSレベルの変更)を処理することで、プラットフォームにネイティブに感じられ、かつ拡張も容易なスイッチャーを構築できます。パターンは小さく、追加する新しいテーマやコンポーネントごとにそのメリットは積み重なっていきます。

FAQ

どちらも機能しますが、data-theme 属性は修飾子のリストではなく単一の値を表現するため、一般的によりクリーンです。クラスは、複数のフラグが共存するコンポジショナルな状態により適しています。テーマでは通常、一度に1つのアクティブな値を持つため、属性セレクタのセマンティクスとより自然にマッチします。

そのフラッシュは、JavaScriptがブラウザによってデフォルトのスタイルでページがすでに描画された後に実行されるときに発生します。修正方法は、head内の小さな同期インラインスクリプトで、localStorageから保存された設定を読み取り、レンダリングが行われる前に data-theme 属性を設定することです。初回訪問者のためには、prefers-color-scheme もチェックすることで、最初のレンダリングが即座にシステムテーマに一致するようにできます。

はい。CSS変数は、スペーシングスケール、ボーダー半径、シャドウ、フォントスタック、アニメーション時間など、任意の有効なCSS値を保持できます。これにより、密度(コンパクトと快適なレイアウト)、モーション設定、あるいはタイポグラフィのバリアントなどのテーマ化に役立ちます。変数は色の参照だけでなく、デザイントークンとして広く扱いましょう。

モダンブラウザ(Chrome 123+, Safari 17.5+, Firefox 120+)ではうまく機能しますが、古いバージョンではサポートが欠けています。モダンなオーディエンスを持つプロジェクトではクリーンな選択肢です。より幅広い互換性のためには、data-theme 属性のアプローチの方が依然として安全であり、light-dark() 単独ではできない3つ以上のテーマへのスケールも可能です。

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..

OpenReplay