Next.jsブログにライト/ダークモード切り替えを実装する方法
- この記事で分かること
- 自前でライト/ダーク切り替えを実装するときの、CSS変数の設計・設定の保存・ちらつき防止(FOUC対策)という3つの考え方とコード
- 実際に使った環境
- Next.js App Router / React useSyncExternalStore。このブログの本番実装(ヘッダーのテーマ切り替えボタン)と同じ構成です
- 筆者の結論
- 切り替えボタン自体の実装より、ページを開いた瞬間に一瞬だけ色がちらつく問題(FOUC)への対策の方が実装の肝でした
ブログを作るとき、「端末のダークモード設定に自動で追従してほしいけど、手動でも切り替えられるようにしたい」というのはよくある要望です。このブログでも実際にこの仕組みを実装しているので、その考え方とコードを紹介します。
🛠 実装してみて分かったこと
実装してみて分かったのですが、この機能で一番厄介なのは「切り替えボタンをクリックしたときの見た目」ではなく、「ページを開いた瞬間に一瞬だけ違う色がちらつく」問題(俗に言うFOUC)でした。サーバー側では常にライトモードでHTMLを組み立てるため、ユーザーがダーク固定を選んでいると、読み込み直後の一瞬だけ白い画面が見えてしまいます。これを防ぐために、Reactが動き出す前に小さなスクリプトで先に色を決めてしまう、という一見地味な工夫が必要でした。見た目上は本当に些細な違いですが、体感の質はかなり変わる部分だと思います。
実装の考え方
- CSS変数でライト/ダークの配色を定義する:
:rootにライト用の値、prefers-color-scheme: darkのメディアクエリにダーク用の値を定義します - 手動切り替え用の属性を用意する:
<html data-theme="dark">のように属性を付け、CSSセレクタでこの属性がある場合は端末設定より優先させます - 選択内容をlocalStorageに保存する: 再訪問時も同じ設定が復元されるようにします
- ちらつき防止スクリプトを仕込む: 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点だけで、逆に言えばそこさえ乗り越えれば残りは素直に実装が進みます。このブログでも実際にこの構成のまま運用しています。