ダークモードの実装について

prefers-color-scheme は、CSSメディアクエリ の一つで、ユーザーのデバイスやブラウザのテーマ設定(ライトモード/ダークモード) に基づいて、ウェブサイトのスタイルを動的に変更するための機能です。
上記を利用すれば、CSSだけでライトモードとダークモードのスタイルを実装できる。ただ、これだけでは手動での切り替えができない。

手動でテーマを切り替える機能を実装するには、親要素に設定するclassに応じたCSSが適用されるようにすれば、シンプルに実装できる。
最近だと color-scheme プロパティで設定できますね。
※ 手動(light もしくは dark を指定)もシステム側に選択させること(light dark と両方を指定)も可能 逆に UA側の上書きを防止すること(only と設定する)も可能
色だけなら light-dark() 色関数がありますね。
現在の color-schemeの値によっての切り替えは @container の style() の拡張待ちですね。(現在はカスタムプロパティしか対応していませんが、いずれは通常のCSS宣言も対応する予定の様です。

とてつもなく勉強になります!
コメント嬉しいです🤩

Tailwind CSSなら、ダークモードは容易に実装できる。
// prefixとしてdark:を付与したclassを設定する
<div class="bg-white dark:bg-gray-800">
<h1 class="text-gray-900 dark:text-white">タイトル</h1>
</div>
module.exports = {
// darkMode: 'media', // OSの設定に従う場合
darkMode: 'selector', // ダークモードを手動で切り替える場合
}

Next.jsでTailwind CSSを利用する前提で、テーマ設定(ライトモード/ダークモード)を手動切り替える機能を実装する。
"use client";
import { useEffect, useState } from "react";
import { Moon, Sun } from "lucide-react";
export const DarkModeToggle = () => {
// テーマ設定(true: ダークモード, false: ライトモード)
const [isDark, setIsDark] = useState<boolean | null>(null);
// 画面が最初に表示される時に一度だけ実行される処理
useEffect(() => {
if (typeof window !== "undefined") {
// ブラウザで実行されていることを確認
const darkMode = localStorage.getItem("darkMode");
if (darkMode === null) {
// ブラウザに保存された設定がない場合、
// ユーザーのPCで設定されているテーマ設定(ダークモード/ライトモード)を取得
const query = window.matchMedia("(prefers-color-scheme: dark)");
setIsDark(query.matches);
} else {
// 前回の設定が保存されている場合、それを使用
setIsDark(darkMode === "true");
}
}
}, []);
// ボタンがクリックされた時の処理
const toggleDarkMode = () => {
setIsDark((prev) => {
const newDarkMode = !prev;
// ページ全体の見た目を切り替え
if (newDarkMode) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
// 次回アクセス時のために設定をブラウザに保存
localStorage.setItem("darkMode", newDarkMode.toString());
return newDarkMode;
});
};
return (
<button
onClick={toggleDarkMode}
className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
aria-label="ダークモード切り替え"
>
<div className="h-5 w-5">
{/* 現在のテーマ設定に応じてアイコンを切り替え */}
{isDark === true && <Moon className="text-gray-700" />}
{isDark === false && <Sun className="text-white" />}
</div>
</button>
);
};
ボタンを押して一度適用したテーマはlocalStorageを用いて記録させ、二度目のアクセスでは初回からそのテーマを適用する機能も合わせて搭載している。

初回ページアクセス時の画面のチラつき(ダークモードを設定しているのにライトモードの画面が一瞬見えてしまう)問題は以下実装で解決できる。
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const isDark = localStorage.getItem('darkMode') === 'true' || (localStorage.getItem('darkMode') === null && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) document.documentElement.classList.add('dark');
})();
`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}
あえてscriptタグで設定するのがポイント。上記で設定しているjavascriptの詳細は以下参照。
(function () {
// ダークモードの設定を確認
const isDark =
// ①ブラウザに保存された設定をチェック
localStorage.getItem("darkMode") === "true" ||
// ②保存された設定がない場合(null)は、
// ユーザーのシステム設定(ダークモード/ライトモード)を使用
(localStorage.getItem("darkMode") === null &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
// ダークモードが有効な場合、ページ全体にdarkクラスを追加
if (isDark) document.documentElement.classList.add("dark");
})();