2021-05-02

ダークモードっていいよね

Next.jsでブログにダークモードを作ったので簡単に解説する

post image

この間ブログにダークモードを実装してみた。 Reactでは結構簡単にできたのでサンプルコードを含めてできるだけ解説する。

ダークモードとは

It means that, instead of the default dark text showing up against a light screen (known as ‘light mode’), a light colour text (white or grey) is presented against a dark or black screen.

普段使われる明るい背景に暗い文字ではなく、暗い背景に明るい文字を表示させること。 自分はウェブやアプリ関係なくダークモードで設定するのが好きだが、今更自分のブログにダークモードがなかったのでNext.jsで簡単に作ってみた。

forbes | what is dark mode

モードを設定する

ブログにモードの設定をしていればその設定に従い、設定がなければユーザーのブラウザの設定に従う。 ここは好みだが、どっちも 設定されてなければデフォルトでdarkにする

import React from "react";

export default function ThemeScript() {
  return (
    <script
      dangerouslySetInnerHTML={{
        __html: blockingSetInitialColorMode,
      }}></script>
  );
}

const blockingSetInitialColorMode = `(function() {
	${setInitialColorMode.toString()}
	setInitialColorMode();
})()
`;

function setInitialColorMode() {
  function getInitialColorMode() {
    const persistedColorPreference = window.localStorage.getItem("theme");
    const hasPersistedPreference = typeof persistedColorPreference === "string";

    if (hasPersistedPreference) {
      return persistedColorPreference;
    }

    const mql = window.matchMedia("(prefers-color-scheme: dark)");
    const hasMediaQueryPreference = typeof mql.matches === "boolean";

    if (hasMediaQueryPreference) {
      return mql.matches ? "dark" : "light";
    }

    return "dark";
  }

  const colorMode = getInitialColorMode();
  const root = document.documentElement;
  root.style.setProperty("--initial-color-mode", colorMode);

  if (colorMode === "dark") document.documentElement.setAttribute("data-theme", "dark");
}

アプリケーションのテーマの設定は localStorageに保存しておく 。 localStorageに入れておくことで、再訪問時にも前回と同様なモードで見ることができる。

ユーザーのブラウザなどの設定を読みことのが prefers-color-scheme モダーンブラウザではサポートされているので使うのに問題はないと思う。

スクリプト配置

ドキュメント(_document)に このスクリプトを入れておく。 画面のRenderingの前に設定をするのでbodyの先頭に入れておく。

export default class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head />
        <body>
          <ThemeScript />
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

モードを適応させる

設置したモードをアプリケーション側で使うには、 Contextを使って、下のレイヤーで参照する

一つ目の useEffect で、設定されたモードをモードとして使わせる。 二つ目の useEffect で、document側にモードを設定させることで実際の色を切り替えることができる。 これはこの後のモードごとに色を設定する部分で実際に使われる。

下位のレイヤーでこのコンテキストを使って色を切り替えるので、colorModeを変更する toggleColorMode 関数も用意しておく。そうすることで他のComponentでは色の変更処理を意識することなく、関数を呼び出すだけで色がしてできる。

prefers-color-scheme を使ってユーザーの設定を読み込んでるのでアプリ内で切り替えるのは無駄かもしてないが、ブラウザによっては設定できないようなのであっても良いと思う。

import React, { createContext, useEffect, useState } from "react";

type ContextProps = {
  colorMode: string;
  toggleColorMode: () => void;
};

export const ThemeContext = createContext({} as ContextProps);

export const ThemeProvider = ({ children }) => {
  const [colorMode, setColorMode] = useState(undefined);

  useEffect(() => {
    const root = window.document.documentElement;
    const initialColorValue = root.style.getPropertyValue("--initial-color-mode");
    setColorMode(initialColorValue);
  }, []);

  useEffect(() => {
    if (colorMode !== undefined) {
      if (colorMode === "dark") {
        document.documentElement.setAttribute("data-theme", "dark");
      } else {
        document.documentElement.removeAttribute("data-theme");
      }
    }
  }, [colorMode]);

  const toggleColorMode = () => {
    const theme = colorMode === "dark" ? "light" : "dark";

    window.localStorage.setItem("theme", theme);
    setColorMode(theme);
  };

  return <ThemeContext.Provider value={{ colorMode, toggleColorMode }}>{children}</ThemeContext.Provider>;
};

ThemeProviderを配置

Providerは _appの下に配置しておく。

function App({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

モードごとに色を切り替える

これは使うCSSのライブラリーによって使い方は変わると思うが、自分はEmotionを使って説明する。 上記の手順でモードをアプリ側に適応させることはできてるので、色を指定することが簡単にできる。

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <Global styles={color} />
      </Head>
      <ThemeProvider>
        <Component {...pageProps} />
      </ThemeProvider>
    </>
  );
}

const color = css`
  :root {
    --color-text: black;
    --color-background: white;
  }

  [data-theme="dark"] {
    --color-text: white;
    --color-background: black;
  }
`;

ThemeProviderでドキュメント側に data-theme="dark" の設置をしているので、CSS側では簡単にVariablesをしてして、それを読み込ませるだけ。

使う側では color: var(--color-text); のように書くだけで良い。

モードを切り替える

モードを切り替える際は、ThemeContextで作った toggleColorMode を呼び出すだけ。

import React, { useContext } from "react";
import { ThemeContext } from "./ThemeProvider";

export default function ToggleTheme() {
  const { colorMode, toggleColorMode } = useContext(ThemeContext);

  return (
    <label>
      <input
        type="checkbox"
        checked={colorMode === 'dark'}
        onChange={() => toggleColorMode()}
      />{' '}
      Dark
    </label>
  );
}

まとめ

ダークモードが本当に良いか否かの議論はあるものの、個人的には好きなのでまぁ良いかなって感じに作ってみた。

完全に余談だけど、実装自体は簡単だったが、ダークモードは色が限られるのでデザインが難しいと感じた。

コックスによれば、読みやすさにとって重要なのは、配色よりも文字と背景のコントラストを強くすることだという。通常モードでもダークモードでも、コントラストが同じなら、おそらく読みやすさに違いはない。しかしわたしたちは、白い背景に黒い文字という組み合わせに慣れているため、通常モードのほうが、やや読みやすく感じるかもしれないとコックスは話す。

こんな感じに読みやすいようなデザインをするのって本当に難しいと今回実装しながら思った。

How to add dark mode to a Gatsby site 「ダークモード」は、本当に“目に優しい”のか?