🐡

【NextJs14】NextJs14 と 便利なライブラリ【#7DarkMode】

2023/12/18に公開

【#7DarkMode】

YouTube: https://youtu.be/sBxCYwxkjM4

https://youtu.be/sBxCYwxkjM4

今回はアプリケーションにダークモードのテーマを実装します。

https://ui.shadcn.com/docs/dark-mode/next

こちらはshadcn-uiに限らずtailwindを使用している
アプリで共通の実装方法になります。

まずはこちらをインストールします。

npm install next-themes

次にプロバイダーを作成します。

components/theme-provider.tsx
"use client";

import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";

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

プロバイダーをレイアウトに反映します。

app/layout.tsx
import type { Metadata } from "next";
import { ClerkProvider } from "@clerk/nextjs";
import { Inter } from "next/font/google";
import "./globals.css";

import { ThemeProvider } from "@/components/theme-provider";

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <ClerkProvider>
        <body className={`${inter.className} bg-white dark:bg-slate-800`}>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </ClerkProvider>
    </html>
  );
}

次にモードを切り替えるボタンを作成します。
まずは、shadcn-uiからdropdown-menuをインストールします。

npx shadcn-ui@latest add dropdown-menu
components/mode-toggle-button.tsx
"use client";

import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";

import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export function ModeToggle() {
  const { setTheme } = useTheme();

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          variant="outline"
          size="icon"
          className="dark:bg-slate-800 focus-visible:ring-0 focus-visible:ring-offset-0"
        >
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          Light
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          Dark
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

今回は上記ボタンを(main)でグループされたページで使用しますので
(main)にlayout.tsxを作成します。

app/(main)/layout.tsx
import React from "react";
import { MainNavbar } from "./_components/main-navbar";

const MainLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <MainNavbar />
      {children}
    </div>
  );
};

export default MainLayout;

ボタンを設置している上記のMainNavbarコンポーネントは
(main)のグループだけで使用しますので、
そのような場合には(main)の直下に「_components」フォルダを作成して
その中でコンポーネントファイルを作成します。

app/(main)/_components/main-navbar.tsx
import { ModeToggle } from "@/components/mode-toggle-button";
import { UserButton } from "@clerk/nextjs";

export const MainNavbar = () => {
  return (
    <nav className="w-full h-auto px-4 py-3 flex items-center justify-between bg-slate-200 dark:bg-slate-900">
      <div>Main Page</div>
      <div className="flex items-center justify-center gap-x-2">
        <ModeToggle />
        <UserButton afterSignOutUrl="/" />
      </div>
    </nav>
  );
};

レイアウト追加に伴いプロテクトページの変更を行います。

app/(main)/protected/page.tsx
import { UserButton, auth, currentUser, UserProfile } from "@clerk/nextjs";
import Image from "next/image";

const ProtectedPage = async () => {
  const { userId } = auth();
  const user = await currentUser();

  return (
    <div className="flex flex-col p-10">
      <div className="flex items-center gap-x-2">
        <UserButton afterSignOutUrl="/" />
        <p>User ID: {userId}</p>
      </div>
      <ul className="flex flex-col p-6">
        <li>
          User Name: {user?.lastName} {user?.firstName}
        </li>
        <li>User Email: {user?.emailAddresses?.[0].emailAddress}</li>
        <li className="flex gap-x-2">
          User Image:{" "}
          <Image src={user?.imageUrl!} width={30} height={30} alt="User" />
        </li>
      </ul>
      <UserProfile />
    </div>
  );
};

export default ProtectedPage;

Discussion