🌙

【Next.js】ダークモード対応を実装してみる

2024/01/27に公開

初めに

今回は Next.js で作ったアプリケーションでダークモードの切り替えを行うための実装を共有したいと思います。

なお、Next.js に関しては歴が非常に浅いため、誤り等あれば指摘していただければ幸いです。

記事の対象者

  • Next.js 初学者
  • Next.js のダークモード対応をしたい方

使用技術

  • Next.js : 13.5.6
  • Material UI : 5.15.3

目的

今回は以下のようにボタンひとつでライトモード / ダークモードの切り替えを行い、それを全てのページに反映させることを目的とします。

実装

実装は以下の手順で進めていきたいと思います。

  1. モードの切り替え、保持をする機能を作成
  2. 切り替えた結果を全ページに反映
  3. モードの切り替えを行うボタンの作成

1. モードの切り替え、保持をする機能を作成

まずはダークモードの切り替えを行う CustomThemeProvider を作成します。
コードは以下の通りです。

theme_context.tsx
import React, {
  createContext,
  useState,
  useMemo,
  useContext,
  ReactNode,
} from "react";
import {
  createTheme,
  ThemeProvider as MUIThemeProvider,
} from "@mui/material/styles";

const ColorModeContext = createContext({ toggleColorMode: () => {} });

interface CustomThemeProviderProps {
  children: ReactNode;
}

export const CustomThemeProvider = ({ children }: CustomThemeProviderProps) => {
  const [mode, setMode] = useState<"light" | "dark">("light");

  // テーマを切り替える関数
  const colorMode = useMemo(
    () => ({
      toggleColorMode: () => {
        setMode((prevMode) => (prevMode === "light" ? "dark" : "light"));
      },
    }),
    []
  );

  const theme = useMemo(
    () =>
      createTheme({
        palette: {
          mode: mode,
        },
      }),
    [mode]
  );

  return (
    <ColorModeContext.Provider value={colorMode}>
      <MUIThemeProvider theme={theme}>{children}</MUIThemeProvider>
    </ColorModeContext.Provider>
  );
};

export const useCustomTheme = () => useContext(ColorModeContext);

詳しく見ていきましょう。
以下の部分では createContext メソッドを使って、テーマモードを切り替えるためのコンテキストを作成しています。初期値は toggleColorMode プロパティを持つオブジェクトです。
createContext はアプリケーション全体でデータを共有するために使用され、useContext などによってアクセスすることができます。

const ColorModeContext = createContext({ toggleColorMode: () => {} });

以下の部分では useState を用いて mode とその状態を変更するための setMode を定義しています。また、初期値は light として設定しています。

const [mode, setMode] = useState<"light" | "dark">("light");

以下の部分では useMemotoggleColorMode を用いて、ダークモードの切り替えを行うための処理を記述しています。
useMemo では引数に指定している関数の返り値をキャッシュしています。今回の場合は、toggleColorMode が指定されていますが、この関数は返り値が void であるため、第二引数の dependencies は空のリストになっています。

  const colorMode = useMemo(
    () => ({
      toggleColorMode: () => {
        setMode((prevMode) => (prevMode === "light" ? "dark" : "light"));
      },
    }),
    []
  );

以下の部分では先述の useMemo を用いてカラーテーマを作成しています。
また、mode を第二引数に指定することで mode の値が変更された際に再度 createTheme が実行されるようになっています。

  const theme = useMemo(
    () =>
      createTheme({
        palette: {
          mode: mode,
        },
      }),
    [mode]
  );

以下の部分では、先ほどまでで作成した colorModetheme をそれぞれ ColorModeContext.ProviderMUIThemeProvider に渡しています。

  return (
    <ColorModeContext.Provider value={colorMode}>
      <MUIThemeProvider theme={theme}>{children}</MUIThemeProvider>
    </ColorModeContext.Provider>
  );

2. 切り替えた結果を全ページに反映

次に、先ほど作成した CustomThemeProvider を全ページに反映させます。
コードは以下の通りです。
MyApp で返す値を CustomThemeProvider で囲むことで、全てのページにダークモードの変更を反映させることができます。

_app.tsx
import "./styles/globals.css";
import type { AppProps } from "next/app";
import { CustomThemeProvider } from "@/components/theme_context";

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

export default MyApp;

3. モードの切り替えを行うボタンの作成

最後にモードの切り替えを行うボタンを作成します。
コードは以下の通りです。

theme_mode_button.tsx
import React from "react";
import { styled, useTheme } from "@mui/material/styles";
import Brightness2OutlinedIcon from '@mui/icons-material/Brightness2Outlined';
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import { IconButton } from "@mui/material";
import { useCustomTheme } from "./theme_context";

const DarkModeIcon = styled(Brightness2OutlinedIcon)(({theme}) => ({
  strokeWidth: 0.5,
  color: "#2CD4BF"
}))

const LightModeIcon = styled(LightModeOutlinedIcon)(({theme}) => ({
  strokeWidth: 0.5,
  color: "#252529"
}))

export const ThemeModeButton = () => {
  const theme = useTheme();
  const { toggleColorMode } = useCustomTheme();

  return (
    <IconButton onClick={toggleColorMode} color="inherit">
      {theme.palette.mode === "dark" ? (
        <DarkModeIcon />
      ) : (
        <LightModeIcon />
      )}
    </IconButton>
  );
};

以下のコードでは、ライトモード/ダークモードの両方のアイコンを設定しています。

const DarkModeIcon = styled(Brightness2OutlinedIcon)(({theme}) => ({
  strokeWidth: 0.5,
  color: "#2CD4BF"
}))

const LightModeIcon = styled(LightModeOutlinedIcon)(({theme}) => ({
  strokeWidth: 0.5,
  color: "#252529"
}))

以下のコードではダークモードを切り替えるボタンについて、 onClicktoggleColorMode を指定しています。また、theme.palette.mode === "dark" とすることで現在のモードがダークモードかどうかを判定することができます。以下では表示させるアイコンを変更しています。

    <IconButton onClick={toggleColorMode} color="inherit">
      {theme.palette.mode === "dark" ? (
        <DarkModeIcon />
      ) : (
        <LightModeIcon />
      )}
    </IconButton>

まとめ

最後まで読んでいただいてありがとうございました。

今回は Next.js × Material UI のダークモード対応について簡単に実装しました。
最近ではダークモード対応しているサイトが多いので、ご参考になれば幸いです。
誤っている点や他の実装方法等あればご指摘いただけると幸いです。

参考

useMemo
https://ja.react.dev/reference/react/useMemo

Discussion