はじめに:なぜダークモードトグルは複雑になったのか
サイトのヘッダーにダークモードトグルを1つ置くだけなのに、選択肢が3つある。ライト、ダーク、システム。一見親切に見えますが、実態はユーザーの本来の目的(コンテンツ閲覧)を妨げるUIになりがちです。
ここで投げかけられる問いはシンプルです。「システムオプションをわざわざ表示する必要があるのか?」 優れた2状態コントロールが1つあれば、3つの状態すべてを表現できます。ユーザーは上書きが必要になった瞬間に1回クリックするだけで済むのです。
基本原則はこうです。設定がなければシステム設定に従い、上書きが必要ならその時点で保存する。 localStorageに値がなければシステムに従い、あればその値を優先する、という設計ですね。

実装:2状態トグルで3状態を表現する方法
以下はシステム設定をデフォルトとして扱い、ユーザーが明示的に上書きした場合のみlocalStorageに保存するパターンです。
// システムのダークモード判定
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// 保存された上書き値(なければnull → システムに従う)
const storedOverride = localStorage.getItem('theme-override');
// 実際に適用するテーマを決定
const effectiveTheme = storedOverride ?? (systemPrefersDark ? 'dark' : 'light');
document.documentElement.dataset.theme = effectiveTheme;
// トグルボタン:現在のテーマの「逆」のみ表示
function renderToggle() {
const button = document.getElementById('theme-toggle');
// ダークなら「ライトへ」、ライトなら「ダークへ」
button.textContent = effectiveTheme === 'dark' ? '☀️ ライト' : '🌙 ダーク';
}
renderToggle();
// クリック時:システムと異なる場合のみ上書きを保存
function onToggle() {
const next = effectiveTheme === 'dark' ? 'light' : 'dark';
const systemTheme = systemPrefersDark ? 'dark' : 'light';
if (next === systemTheme) {
// システム値と一致したら上書きを削除 → システムに復帰
localStorage.removeItem('theme-override');
} else {
localStorage.setItem('theme-override', next);
}
location.reload(); // または状態更新ロジック
}
ポイントはトグルボタンが常に「逆の状態」のみを表示する点です。ダーク状態なら「ライトへ」ボタンのみ、ライト状態なら「ダークへ」ボタンのみが見えます。システムと同じ値に戻れば上書きを削除し、自然にシステム設定へ復帰します。
システム設定の変更を検知する
ユーザーがOSでダークモードを切り替えたら、リアルタイムで反映される必要があります。
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
// 上書きがない場合のみシステム変更を反映
if (!localStorage.getItem('theme-override')) {
document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
renderToggle();
}
});
これで3状態トグルなしでもユーザー体験は完結します。

注意点と3状態が正当化される例外ケース
1. このパターンは「常時表示トグル」に限定される
ヘッダーやナビゲーションに常時表示されるトグルなら2状態が正解です。しかし別の設定パネルに置くオプションなら3状態も許容されます。ユーザーが既に「設定を変えに来た」という明確な意図を持っているからです。
2. カラースキームが3つ以上ある場合
ライト/ダーク以外に「ハイコントラストモード」「セピア」など追加スキームがあるなら、システム設定だけでは不足します。この場合は明示的な選択UIが必要でしょう。
3. トグル状態の「曖昧さ」問題
ライトのシステム設定からダークにトグルし、再びライトに戻したとき、これが「上書きでライト」なのか「システム復帰」なのか曖昧になります。上記コードのようにシステム値と一致したら上書きを削除する方式で処理すれば、この曖昧さは消えます。ユーザーが気づきにくいエッジケースですが、コード1行で整理できます。
4. Qiitaコミュニティでの議論動向
Qiitaでは「prefers-color-schemeの挙動がブラウザごとに微妙に異なる」「SSR時にちらつき(FOUC)が発生する」といった実装上の課題がよく共有されています。特にNext.jsのSSR環境では、サーバー側でシステム設定を取得できないため、初回レンダリング時のちらつき対策としてインラインスクリプトでdocument.documentElement.dataset.themeを早期設定する手法が定番化しています。

まとめ:UIは少ないほど良い
整理するとこうなります。
- 常時表示トグル → 2状態(現在の逆のみ表示)、システム設定をデフォルト
- 設定パネル内オプション → 3状態許容
- カラースキーム3つ以上 → 明示的な選択UIが必要
トグルはユーザーの本来の目的とは完全に無関係なコントロールです。コンテンツを読みに来た人に選択肢3つを押し付ける代わりに、システムに任せ、必要時に1クリックで解決させましょう。UIが減れば認知負荷も減り、コードもシンプルになります。
関連記事として、最新CSS機能の実務適用を検討しているなら2025年CSS最新機能まとめガイドを、認証レイヤーでゼロトラストを検討中ならCloudflare Sandboxによるエージェント認証ガイドを参照してください。