React/Next.jsを使用して、よくあるテーマカラーの切り替えボタンの実装をめちゃ簡単に解説する




# 1. 概要
簡単なアニメーションの挙動
https://x.com/Nao8epicmotion/status/1780411806277919134

ソースコード
https://github.com/Kroro1208/nextjs-portfolio-website.git

React/Next.jsを使用してwebsiteの開発を行う際に、よく出てくる***ライトモードとダークモード***の切り替えを解説します。

もうこれはパターン化できるもので、一度覚えてしまえば使いまわしたりして表現できる幅も広がると思います。この際に覚えちゃいましょう。


# 2. 必要なコンポーネントとファイル
今回のテーマに関係ある部分だけ解説させていただきます。
iconはReact iconを使用してます。


## app/layout.jsx
ここで後に作成する`ThemeProvider`コンポーネントでレンダリングされる部分を囲います。
```react
import { Inter } from "next/font/google";
import "./globals.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { ThemeProvider } from "@/components/ThemeProvider";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={inter.className}>
        <ThemeProvider attribute="class" defaultTheme="light">
          <Header />
          {children}
          <Footer />
        </ThemeProvider>
      </body>
    </html>
  );
}
```

## app/Home.jsx
```react
import About from "@/components/About";
import Cta from "@/components/Cta";
import Hero from "@/components/Hero";
import Reviews from "@/components/Reviews";
import Services from "@/components/Services";
import Works from "@/components/Works";

export default function Home() {
  return (
    <main>
      <Hero />
      <About />
      <Services />
      <Works />
      <Reviews />
      <Cta />
    </main>
  );
}

```

## components/Header.jsx
※ここのuseEffectはスクロールアニメーションに関係するので今回は関係ありません。
動画にあるスクールの時に、遅れてニュルっと出てくるオレンジのBarのやつです。

 ```react
 "use client";
import { useEffect, useState } from "react";
import Logo from "./Logo";
import MobileNav from "./MobileNav";
import Nav from "./Nav";
import ThemeToggle from "./ThemeToggle"
import { usePathname } from "next/navigation";

const Header = () => {
    const [header, setHeader] = useState(false);
    const pathname = usePathname();

    useEffect(() => {
        const scrollYPos = window.addEventListener('scroll', () => {
            window.scrollY > 50 ? setHeader(true) : setHeader(false);
        });

        return () => window.removeEventListener('scroll', scrollYPos);
    });

    return (
        <header className={`${header ? "py-4 bg-gray-200 shadow-lg dark:bg-accent" : "py-6 dark:bg-transparent"}
        sticky top-0 z-30 transition-all ${pathname === "/" && "bg-sky-200"}`}>
            <div className="container mx-auto">
                <div className="flex justify-between items-center">
                    <Logo />
                    <div className="flex items-center gap-x-6">
                        <Nav
                            containerStyles="hidden xl:flex gap-x-8 items-center"
                            linkStyles="relative hover:text-primary transition-all"
                            underlineStyles="absolute left-0 top-full h-[2px] bg-primary w-full"
                        />
                        <ThemeToggle />
                        <div className="xl:hidden">
                            <MobileNav />
                        </div>
                    </div>
                </div>
            </div>
        </header>
    )
}

export default Header

 ```


 ## components/ThemeProvider.jsx
 ここでライブラリをインポートして`NextThemeProvider`として返します。
 この使い方がちょっとだけ難しいかもですが、`...props`には`layout.jsx`に記載している`<ThemeProvider>`内の`attribute="class" defaultTheme="light"`が渡ってきます。
 この二つのpropsは`ThemeProvider`を使用するときの書き方みたいなものです。



 
 ```react
 "use client";
import { ThemeProvider as NextThemeProvider } from "next-themes";

export function ThemeProvider({ children, ...props }) {
    return <NextThemeProvider {...props}>{children}</NextThemeProvider>;
}

 ```
 ## components/ThemeToggle.jsx
 rotateとscaleを使用してアイコンをクリックしたときに回転させてちょっとオシャレに。
 ボタンにvariant="outline"を指定して、枠線だけが表示させ中身が透明な「アウトラインスタイル」を持たせてます。

 ```react
 "use client";
import { Button } from "./ui/button";
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
import { useTheme } from "next-themes";

const ThemeToggle = () => {
    const { theme, setTheme } = useTheme();
    return (
        <Button variant="outline" size="icon" onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
            <SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
            <MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-0 scale-0 transition-all dark:rotate-0 dark:scale-100" />
        </Button>
    )
}

export default ThemeToggle

 ```

 ## app/global.css
ここでデフォルトカラーとdarkモード時に必要となるカラーを定義する
 ```react
 @tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  @layer base {
    :root {
      --background: 0 0% 100%;
      --primary: 8 99% 67%;
      --primary-foreground: 300 0% 100%;
      --secondary: 240 19% 16%;
      --secondary-foreground: 300 0% 100%;
      --border: 8 82% 92%;
      --input: 8 82% 92%;
      --ring: 9 99% 67%;
      
      他の設定
    }
    .dark {
      --background: 237 22% 20%;
      --primary: 8 99% 67%;
      --primary-foreground: 300 0% 100%;
      --secondary: 240 19% 16%;
      --secondary-foreground: 300 0% 100%;
      --border: 237 22% 23%;
      --input: 237 22% 23%;
      --ring: 8 99% 67%;
      
      他の設定
    }
  }
}



 ```

 ## tailwind.config.js

 ```react
 /** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: ["class"],
  content: [
    './pages/**/*.{js,jsx}',
    './components/**/*.{js,jsx}',
    './app/**/*.{js,jsx}',
    './src/**/*.{js,jsx}',
  ],
  prefix: "",
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "sm": "640px",
        "md": "768px",
        "lg": "1024px",
        "xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}
 ```

 # 以上です！！
 次回は、スクロール時のニュルっと出てくるやつを解説します！