0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js でチェックボックスを使ってダークテーマをいい感じに制御する

Posted at

2.gif

環境

  • Next.js v14.2.3 (App Router)
  • React v18.2.0
  • TypeScript v5.4.5
  • Tailwind CSS v3.4.3
  • daisyUI v4.11.1
  • next-themes v0.3.0

仕様

  • テーマ
    • light
    • dark
    • system(デフォルトテーマ。OS が通常モードなら light、ダークモードなら dark)
  • サイトへの初回アクセス時:system
  • チェックボックス・オン:dark
  • チェックボックス・オフ:light
  • Web ブラウザのローカルストレージに現在のテーマ設定を保存し、最後に設定したテーマを次回アクセス時に引き継ぐ

前提

  • html 要素における data-theme 属性の値が light のときに、通常のテーマに対応した CSS スタイルが適用される状態になっている
  • html 要素における data-theme 属性の値が dark のときに、ダークテーマに対応した CSS スタイルが適用される状態になっている

※ たとえば daisyUI などを使用すると、簡単にこの前提を満たせます。

共通:next-themes を設定する

src/app/layout.tsx
import "~/styles/globals.css";

import { ThemeProvider } from "next-themes";
import { Footer } from "./_components/footer";
import { Header } from "./_components/header";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    // suppressHydrationWarning が必要
    <html lang="ja" suppressHydrationWarning>
      <body>
        <ThemeProvider>
          <Header />
          {children}
          <Footer />
        </ThemeProvider>
      </body>
    </html>
  );
}

例 1:next-themes とチェックボックスを使って、シンプルなテーマコントローラーを実装する

1.gif

src/app/_components/theme-controller.tsx
"use client";

import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export function ThemeController() {
  const [mounted, setMounted] = useState(false); // この state がポイント
  const [isSystemDarkMode, setIsSystemDarkMode] = useState(false);
  const { theme, setTheme } = useTheme();
  useEffect(() => {
    setMounted(true);
    setIsSystemDarkMode(
      window.matchMedia("(prefers-color-scheme: dark)").matches,
    );
  }, []);

  return mounted ? (
    <input
      type="checkbox"
      value="dark"
      className="h-3.5 w-3.5"
      checked={theme === "system" ? isSystemDarkMode : theme === "dark"}
      onChange={(e) => setTheme(e.target.checked ? "dark" : "light")}
    />
  ) : (
    // マウント前後でチェックボックスの位置が変わらないようにする
    <div className="h-3.5 w-3.5"></div>
  );
}

例 2:next-themes、daisyUI、チェックボックスを使って、いい感じのテーマコントローラーを実装する

2.gif

tailwind.config.ts
import { type Config } from "tailwindcss";

export default {
  content: ["./src/**/*.tsx"],
  plugins: [require("daisyui")], // daisyUI プラグインを追加
  daisyui: {
    darkTheme: false, // 手動でテーマを切り替えるため、daisyUI による自動切り替えを無効化
  },
} satisfies Config;
src/app/_components/theme-controller.tsx
"use client";

import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export function ThemeController() {
  const [mounted, setMounted] = useState(false); // この state がポイント
  const [isSystemDarkMode, setIsSystemDarkMode] = useState(false);
  const { theme, setTheme } = useTheme();
  useEffect(() => {
    setMounted(true);
    setIsSystemDarkMode(
      window.matchMedia("(prefers-color-scheme: dark)").matches,
    );
  }, []);

  return mounted ? (
    // https://daisyui.com/components/theme-controller/#theme-controller-using-a-swap
    <label className="swap swap-rotate">
      {/* this hidden checkbox controls the state */}
      <input
        type="checkbox"
        value="dark"
        checked={theme === "system" ? isSystemDarkMode : theme === "dark"}
        onChange={(e) => setTheme(e.target.checked ? "dark" : "light")}
      />

      {/* sun icon */}
      <svg
        className="swap-off h-10 w-10 fill-current"
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
      >
        <path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
      </svg>

      {/* moon icon */}
      <svg
        className="swap-on h-10 w-10 fill-current"
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
      >
        <path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
      </svg>
    </label>
  ) : (
    <div className="h-10 w-10"></div> // マウント前後でチェックボックスの位置が変わらないようにする
  );
}

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?