AIツクリラボ
Next.js制作Tips5分で読めます実装記録

Next.jsブログにライト/ダークモード切り替えを実装する方法

この記事で分かること
自前でライト/ダーク切り替えを実装するときの、CSS変数の設計・設定の保存・ちらつき防止(FOUC対策)という3つの考え方とコード
実際に使った環境
Next.js App Router / React useSyncExternalStore。このブログの本番実装(ヘッダーのテーマ切り替えボタン)と同じ構成です
筆者の結論
切り替えボタン自体の実装より、ページを開いた瞬間に一瞬だけ色がちらつく問題(FOUC)への対策の方が実装の肝でした

ブログを作るとき、「端末のダークモード設定に自動で追従してほしいけど、手動でも切り替えられるようにしたい」というのはよくある要望です。このブログでも実際にこの仕組みを実装しているので、その考え方とコードを紹介します。

🛠 実装してみて分かったこと

実装してみて分かったのですが、この機能で一番厄介なのは「切り替えボタンをクリックしたときの見た目」ではなく、「ページを開いた瞬間に一瞬だけ違う色がちらつく」問題(俗に言うFOUC)でした。サーバー側では常にライトモードでHTMLを組み立てるため、ユーザーがダーク固定を選んでいると、読み込み直後の一瞬だけ白い画面が見えてしまいます。これを防ぐために、Reactが動き出す前に小さなスクリプトで先に色を決めてしまう、という一見地味な工夫が必要でした。見た目上は本当に些細な違いですが、体感の質はかなり変わる部分だと思います。

実装の考え方

  1. CSS変数でライト/ダークの配色を定義する: :rootにライト用の値、prefers-color-scheme: darkのメディアクエリにダーク用の値を定義します
  2. 手動切り替え用の属性を用意する: <html data-theme="dark">のように属性を付け、CSSセレクタでこの属性がある場合は端末設定より優先させます
  3. 選択内容をlocalStorageに保存する: 再訪問時も同じ設定が復元されるようにします
  4. ちらつき防止スクリプトを仕込む: Reactのハイドレーションより前に、保存された設定を<html>に反映させます

CSSの構成

:root {
  --background: #ffffff;
  --foreground: #24262e;
}
 
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --background: #24262e;
    --foreground: #f1ece6;
  }
}
 
:root[data-theme="dark"] {
  --background: #24262e;
  --foreground: #f1ece6;
}

ポイントは、:root[data-theme="dark"]を単独のセレクタとしても用意しておくことです。これにより、端末の設定がライトであっても、ユーザーが明示的にダークを選んだ場合はそちらを優先させられます。

切り替えボタンの実装

ReactのuseSyncExternalStoreを使うと、localStorageのような外部の値と画面表示を安全に同期できます。useEffectの中でsetStateする書き方でも動作はしますが、最近のESLintルールでは推奨されない実装として警告されることがあります。

"use client";
import { useSyncExternalStore } from "react";
 
function getSnapshot() {
  const stored = localStorage.getItem("theme-preference");
  return stored ?? "system";
}
 
function getServerSnapshot() {
  return "system"; // サーバーにはlocalStorageが無いため固定値を返す
}

ちらつき防止スクリプト

<body>の先頭に、保存された設定を即座に<html>へ反映する小さなスクリプトを置きます。

<script>
  (function() {
    try {
      var t = localStorage.getItem('theme-preference');
      if (t === 'light' || t === 'dark') {
        document.documentElement.setAttribute('data-theme', t);
      }
    } catch (e) {}
  })();
</script>

Reactが描画を始める前にこのスクリプトが実行されるため、ダーク固定を選んでいるユーザーに一瞬白い画面が見える、という現象を防げます。

よくある質問

Q. ちらつき防止スクリプトを入れないとどうなりますか? A. ダーク固定を選んでいるユーザーが再訪問したときに、一瞬だけライト画面が表示されてから切り替わる、という見た目のノイズが発生します。致命的な不具合ではありませんが、細部の品質として気になる人には気になる部分です。

実装してみて

CSS変数の設計・設定の保存・ちらつき防止という3つを押さえれば、機能自体の実装はそれほど複雑ではありませんでした。想定より時間がかかったのはちらつき防止の1点だけで、逆に言えばそこさえ乗り越えれば残りは素直に実装が進みます。このブログでも実際にこの構成のまま運用しています。