Next.js でのライト/ダークモード切替

このポートフォリオサイト(shu-web-portfolio)で実際に実装した方法をベースにまとめる。

そもそもの難しさ:Next.js(SSR/静的サイト)特有の問題

ブラウザだけで完結するSPAなら「JSでbodyにクラスを付ける」だけでいいが、Next.jsはサーバー側(またはビルド時)にHTMLを生成する。

この「チラつきをどう消すか」が、Next.jsでダークモード対応する上での最大の技術ポイント。

採用した方法:CSS変数 + data-theme属性 + 同期スクリプト

1. 色をCSS変数(カスタムプロパティ)で定義する

app/globals.css:root に、ライト/ダーク両方の色をあらかじめ変数として用意する。

:root {
  --bg:#fbfcfc; --surface:#f2f6f5; --ink:#141a1f; --muted:#586570; --line:#e4e9ea;
  --accent:#0c8f80; --accent-ink:#0a7568;
}

@media (prefers-color-scheme: dark){
  :root{ --bg:#0e1217; --surface:#151b22; --ink:#e8eef3; --muted:#8b97a4; --line:#242c36;
    --accent:#2fd4c0; --accent-ink:#57e2d1; }
}

:root[data-theme="light"]{ --bg:#fbfcfc; --surface:#f2f6f5; }
:root[data-theme="dark"]{ --bg:#0e1217; --surface:#151b22; }

ポイント:

2. ページ読み込み時に「同期スクリプト」でテーマを即決定する(チラつき対策の本体)

app/layout.tsx の head 部分に、Reactのハイドレーションより前に実行される素のscriptタグを埋め込む。

const themeScript = `
(function(){try{
  var t = localStorage.getItem('theme');
  if (!t) { t = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
  document.documentElement.dataset.theme = t;
}catch(e){}})();
`;

このスクリプト文字列を、レイアウトのheadタグの中で「素のscript要素」としてdangerouslySetInnerHTML経由で埋め込む。JSXの構文自体は次の段落に分けて記載する。

なぜこれでチラつかないのか: