Light and Dark Mode Switch Toggle Menu

A button is added to the header that opens a menu that allows choosing light and dark mode directly, and default to system.


Required dependencies

We use the next-themes library for managing the theme which switches between light and dark mode.

# npm i next-themes --save

We add two Shadcn UI components - for a button and for a dropdown menu.

# npx shadcn-ui@latest add button
# npx shadcn-ui@latest add dropdown-menu

Components

We use the theme provider to create a new provider that manages the theme. src/components/providers.tsx:

"use client";
 
import { ThemeProvider } from "next-themes";
import { ReactNode } from "react";
 
export function Providers({ children }: { children: ReactNode }) {
  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </ThemeProvider>
  );
}

A button that is placed in the header opens a dropdown menu with the options for light, dark, and system-defined mode. src/components/mode-toggle.tsx:

"use client";
 
import { useTheme } from "next-themes";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { Button } from "./ui/button";
import { Moon, Sun } from "lucide-react";
 
export function ModeToggle() {
  const { setTheme } = useTheme();
 
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" className="w-10 px-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>
  );
}

We wrap the content of the layout in the newly created provider. src/app/layout.tsx:

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { SiteHeader } from "@/components/site-header";
import { Providers } from "@/components/providers";
 
const inter = Inter({ subsets: ["latin"] });
 
export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};
 
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className="min-h-screen bg-background font-sans antialiased">
        <Providers>
          <div className="relative flex min-h-dvh flex-col bg-background">
            <SiteHeader />
            <main className="flex-1">{children}</main>
          </div>
        </Providers>
      </body>
    </html>
  );
}

We add the new button that opens the theme mode dropdown to the site header. src/components/site-header.tsx:

import { MainNav } from "./main-nav";
import { ModeToggle } from "./mode-toggle";
 
export function SiteHeader() {
  return (
    <header className="sticky top-0 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
      <div className="container flex h-14 max-w-screen-2xl items-center">
        <MainNav />
        <div className="flex flex-1 items-center justify-end space-x-2">
          <nav className="flex items-center">
            {/* Placeholder for buttons to be added */}
            <ModeToggle />
          </nav>
        </div>
      </div>
    </header>
  );
}


Last Updated: