Nextjs TailwindCSS Tutorial: Add darkmode to your app

Nextjs TailwindCSS Tutorial: Add darkmode to your app

Install dependencies

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

Tailwindcss Config

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

Utils

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))
}

Button component for theme toggling

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 }

Theme Provider

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>
}

Theme Toggle component

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>
  )
}

Putting it all together

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;
  }
}

Layout.tsx

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>
  )
}

Render text

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>
  )
}

Output

Dark mode

Light mode