JOURNAL

スマホの『ダークモード』自動切り替えの仕組み(prefers-color-scheme)

部屋の明かりを消してベッドに入り、リラックスしてスマホを眺めていたら、開いたWebサイトが「真っ白な眩光」を放って目を直撃し、「うわっ眩しっ!」と叫んだ経験はありませんか?

OSをダークモードに設定しているユーザーにとって、サイト側がライトモードのまま突撃してくるのは一種の閃光弾です。今回は、OSのダーク/ライト設定を自動検知するCSSのprefers-color-schemeの基礎から、実戦で破綻しないモダンな実装設計についてツラツラと書きたいと思います。

1. 基本:CSS変数(カスタムプロパティ)で組むのが一番綺麗

prefers-color-schemeのメディアクエリを使えば、たった数行でOSの設定に追従させることができます。実戦では、各要素に色を直書きするのではなく、CSS変数(カスタムプロパティ)を切り替える構成にするのが圧倒的に保守しやすくておすすめです。

※よく、--color-bg: #fdfbf7; というハイフン2つから始まる記述について質問をいただくことがありますが、これはCSSのカスタムプロパティ(CSS変数)を用いた書き方です。あらかじめ名前をつけて定義しておき、後から var(--color-bg) のように呼び出して使い回すことができます。サイト全体のカラーパレットを一元管理できるため、ダークモード対応ではほぼ必須の強力な機能です。詳しい仕様はMDNの「CSSカスタムプロパティ(変数)の使用」にも分かりやすくまとめられています。ちなみに、これを使わないと💀💀わけではありませんが、使った方が絶対に綺麗で保守が楽になります。

/* 1. デフォルト(ライトモード)の配色 */
:root {
  --color-bg: #fdfbf7;
  --color-text: #2c2523;
  --color-card-bg: #ffffff;
  --color-border: #e8e2d8;
}

/* 2. OSがダークモードのときは変数だけを上書き */
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #141211;
    --color-text: #e0d8cc;
    --color-card-bg: #1f1b19;
    --color-border: #38312d;
  }
}

/* 3. 各要素はCSS変数を参照するだけ */
body {
  background-color: var(--color-bg);
  color: var(--color-text);
}
.card {
  background-color: var(--color-card-bg);
  border: 1px solid var(--color-border);
}

この構成にしておけば、HTML側の各要素に @media をいちいち書く必要がなくなり、カラーパレットの変更も :root の数行を書き換えるだけで一括反映できます。

2. 実戦の壁:「自動追従」と「手動切り替えボタン」の両立

「OS設定に自動追従するだけで十分でしょ」と思いがちですが、実際には「OSはダークモードだけど、このサイトは資料として明るい背景で見たい」「外の太陽光の下だから一時的にライトにしたい」というユーザーが確実に存在します。

そのため、本番サービスでは以下の「3段構え」を組むのがベストプラクティスです。

  • システム(自動):OSの prefers-color-scheme に完全追従
  • ライト固定:ユーザーが手動で「明るい」を選択
  • ダーク固定:ユーザーが手動で「暗い」を選択
/* HTMLタグの属性で強制上書きできるようにしておく */
[data-theme="light"] {
  --color-bg: #fdfbf7;
  --color-text: #2c2523;
}
[data-theme="dark"] {
  --color-bg: #141211;
  --color-text: #e0d8cc;
}

3. JavaScriptでの検知と「白飛び(FOUC)」の防止策

手動切り替えボタンを作る場合、JavaScript側で現在のOS設定を拾うにはwindow.matchMedia()を使います。

// 現在のOS設定を判定
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
console.log(darkModeQuery.matches ? 'ダークモード' : 'ライトモード');

// OS設定がリアルタイムに変更されたときの監視
darkModeQuery.addEventListener('change', (e) => {
  if (!localStorage.getItem('user-theme-choice')) {
    // ユーザーの手動固定がなければ自動追従
    applyTheme(e.matches ? 'dark' : 'light');
  }
});

💡 チラつき(FOUC)を防ぐ超重要テクニック

localStorageからテーマを読み込んで適用する処理をDOMContentLoadedや外部JSで遅れて実行すると、「ページを開いた瞬間に一瞬だけ画面が真っ白に光ってからダークになる」という最悪の白飛びが発生します。テーマ判定のスクリプトは、必ず<head>タグ内の最上部にインラインで配置して同期実行するのが鉄則です。

4. スクロールバーもダークにする魔法の1行

CSSの最上部に以下の1行を添えておくだけで、ブラウザ標準のスクロールバーやチェックボックス、セレクトボックスなどのUIパーツもOSに合わせて自動的にダークカラーに最適化されます。

:root {
  color-scheme: light dark; /* ブラウザ標準UIも自動でダーク化 */
}

これがないと、せっかく背景を真っ黒にしたのにスクロールバーだけが白く浮いてしまうという「惜しいダークモード」になってしまいます。

実装手法メリットデメリット・注意点
@media のみCSSだけで完結・最も手軽手動で切り替えたいユーザーに対応できない
CSS変数 + data-theme自動追従と手動固定を両立可能JSでの属性付与・ストレージ保存が必要
color-scheme 併用スクロールバーやフォームも綺麗に統一対応ブラウザの考慮(モダン環境なら◎)

結論:押し付けない優しさが一番心地いい

ダークモード対応は、単に「背景を黒くする」だけではなく、利用者の生活リズムや好みにそっと寄り添うためのアクセシビリティの一環です。

OSの設定を尊重して自動で馴染みつつ、いつでも好みの見た目に切り替えられる自由を残しておく。そんな「押し付けがましくない優しさ」を意識して、道具たちの装いを整えています。