フラッシュしないダークモードに辿り着くまで

WEBのダークモードはローカルストレージに値が格納されるという仕組みで作るとブラウザに保存された値を読み取って再度訪れたときにも前と同じテーマを表示します。

今回作成するに当たって条件としてはbodyにトランジションを当ててもスーパーリロードでもチラつきがないことが最優先でした。

参考にさせていただいたサイトが何件かあるので共有します。 これからダークモードを実装したい方の参考になれば嬉しいです。

james Wallis

Sunday morning

Takuya Matsuyama

The Quest for the Perfect Dark Mode

Next.js(with CSS Modules)アプリにダークモードを実装する (Zenn.dev(minguu))

まずこの上の二人のコードを参考にTailwindCSSのダークモード機能で:darkクラスが当たってるところをトグルさせるというやり方でやってみました。

ところがbodyにトランジションを当てるとどうしてもチラつきが発生します。 これは前からしょうがないと思っていたんですがかなり調べました。

チラつきを抑える工夫はminguu氏の記事がとても参考になったというか実現していたので参考にしました。

この方の記事をよく読んだらdangerouslySetInnerHTMLに最初からダークモードにしておく関数を埋んで対処しています。

documentからbeforeInteractiveでNext Scriptを走らせる方法ばかりヒットしてて、まさかこれでラップするように作ることで実現できるとは感服です。

UI側の切り替えるボタンの実装は最終的に以下のようになりました。 元々作っていたコードをSunday morningさんとjames Wallis氏を参考に作っていたコードを改良したものです。 トグルの真偽値をJSXの中に渡したかったので以下のようになりました。

1import { useState, useEffect } from "react";
2import { AnimatePresence, motion } from "framer-motion";
3
4export const ToggleDarkMode: React.FC = () => {
5  const [darkMode, setDarkMode] = useState(false);
6
7  useEffect(() => {
8    if (
9      localStorage.theme === "dark" ||
10      (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
11    ) {
12      setDarkMode(true);
13      document.documentElement.setAttribute("data-theme", "dark");
14    } else {
15      setDarkMode(false);
16      document.documentElement.setAttribute("data-theme", "light");
17    }
18  }, [darkMode]);
19
20  const handleChangeDarkMode = () => {
21    if (!darkMode) {
22      setDarkMode(true);
23      localStorage.theme = "dark";
24    } else {
25      setDarkMode(false);
26      localStorage.theme = "light";
27    }
28  };
29
30  return (
31    <div className="togglePosition">
32      <div className="iconButton shadow" onClick={handleChangeDarkMode}>
33        <AnimatePresence exitBeforeEnter>
34          <motion.div
35            className="flex"
36            onClick={handleChangeDarkMode}
37            key={darkMode ? "dark" : "light"}
38            initial={{ rotate: -120, opacity: 0 }}
39            animate={{ rotate: 0, opacity: 1 }}
40            exit={{ y: 20, opacity: 0 }}
41            transition={{ duration: 1 }}
42          >
43            <div className={darkMode ? "gg-moon" : "gg-sun"} />
44          </motion.div>
45        </AnimatePresence>
46      </div>
47    </div>
48  );
49};
50
51export default ToggleDarkMode;

もっといい実装方法やチラつきを抑える他の方法、書き方があれば是非教えてもらえると助かります。 それではよきダークモードライフをお過ごしください。