Install the necessary packages for the project using the following commands. The first command adds various libraries to handle class names, animations, and theme management, while the second command installs a package for merging Tailwind CSS utilities as a development dependency.
yarn add clsx class-variance-authority lucide-react next-themes tailwind-merge
yarn add tailwind-css-merge --dev
Configure Tailwind CSS for the project with this snippet. Enable dark mode, specify the locations of content files, and extend the default theme by adding colors. An animation plugin is also included for extra functionality (optional).
//tailwind.config.ts
import type { Config } from 'tailwindcss'
import { fontFamily } from 'tailwindcss/defaultTheme'
const config: Config = {
darkMode: "class",
content: ["src/app/\*\*/\*.{ts,tsx}", "src/components/\*\*/\*.{ts,tsx}"],
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
},
},
},
plugins: [require("tailwindcss-animate")],
}
export default config
Define a utility function that combines class names with this code. Utilize the clsx
and tailwind-merge
packages to allow a concise and flexible way to apply class names throughout the application.
//src/lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Create a reusable Button component using the class-variance-authority
package with this code snippet. Define different variants and sizes for a consistent design across the application. This Button
component will be used later for toggling the theme.
//src/components/ui/Button.ts
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
ghost: "bg-transparent text-primary hover:bg-primary/10",
},
size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
Introduce the ThemeProvider
component with this code, leveraging the next-themes
package to handle theme state in the Next.js application. Wrap components with this provider to enable them to access and modify the theme.
//components/ThemeProvider.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>
}
Define a ThemeToggle
component using the previously created Button
component with this snippet. Include icons for both light and dark modes and toggle the theme when clicked, offering a user-friendly way to switch between themes.
//src/components/ThemeToggle.tsx
"use client"
import * as React from "react"
import { Sun, Moon } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/Button"
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
className="mb-4"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-[1.5rem] w-[1.3rem] dark:hidden" />
<Moon className="hidden h-5 w-5 dark:block" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
Integrate Tailwind CSS and define custom variables for colors with this global CSS file. Set up the default and dark theme styles, linking the Tailwind classes to the custom color variables. The changes in the colors are applied when the dark class is activated.
//src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
}
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
}
}
@layer base {
body {
@apply bg-background text-foreground;
}
}
Construct the main layout for the application with this code, including the theme provider, theme toggle button, and content area. Integrate the global CSS, add meta-information, and set up the layout to be responsive and visually appealing.
//src/app/layout.tsx
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { ThemeProvider } from "@/components/ThemeProvider"
import { ThemeToggle } from '@/components/ThemeToggle'
import { cn } from '@/lib/utils'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={cn(
"min-h-screen bg-background font-sans antialiased",
inter.className
)}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<main className="py-12 max-w-4xl mx-auto">
<div>
<ThemeToggle />
</div>
{children}
</main>
</ThemeProvider>
</body>
</html>
)
}
Since we already added dark mode to our site, the paragraph will have dark color in light mode and light color in dark mode.
//src/app/page.tsx
export default function Home() {
return (
<p>Hello world</p>
)
}
Dark mode
Light mode