Zenixbase
Building a Next.js Blog with Tailwind CSS & ShadCN UI: Setup & Dark Mode

Building a Next.js Blog with Tailwind CSS & ShadCN UI: Setup & Dark Mode

T
Tech & Beyond
December 11, 20257 min read

Part 1: Project Setup and Dark Mode Implementation

Welcome to the first part of our comprehensive series on building a professional blog application from scratch! In this tutorial, we'll create a solid foundation using Next.js 16, Tailwind CSS, and shadcn/ui—three powerful technologies that work beautifully together.

By the end of this guide, you'll have a fully functional Next.js application with a responsive header, multiple pages, and a smooth dark mode toggle that persists across sessions.

What We're Building Today

In this first installment, we'll accomplish the following:

  • Set up a Next.js 16 project with TypeScript and Tailwind CSS
  • Configure shadcn/ui component library
  • Create three main pages (Home, Categories, About)
  • Build a responsive navigation header
  • Implement a fully functional dark mode with next-themes
  • Handle common hydration errors

Prerequisites

Before we begin, make sure you have:

  • Node.js 18.17 or later installed
  • Basic knowledge of React and TypeScript
  • A code editor (VS Code recommended)
  • A terminal/command line interface

Step 1: Creating the Next.js Application

Let's start by creating our Next.js application. Open your terminal and run:

npx create-next-app@latest tech-blog

Note: You can select the "Recommend Configuration"

You'll be prompted with several configuration options. Here's what to choose:

  • TypeScript: Yes (for better type safety)
  • ESLint: Yes (for code quality)
  • Tailwind CSS: Yes (essential for styling)
  • src/ directory: No (keeping it simple)
  • App Router: Yes (using modern Next.js)
  • Import alias: Yes (default @ is fine)

Once the installation completes, navigate into your project:

cd tech-blog
code .

Step 2: Installing shadcn/ui

shadcn/ui is a collection of beautifully designed, accessible components that you can copy directly into your project. It's built on top of Radix UI and Tailwind CSS.

Initialize shadcn/ui in your project:

npx shadcn@latest init

Configuration options:

  • Style: New York
  • Base color: Zinc
  • CSS variables: Yes

This creates the necessary configuration files and sets up your design system.

Step 3: Project Cleanup

Let's clean up the default Next.js boilerplate. Open app/page.tsx and replace it with:

export default function Home() {
  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold">HomePage</h1>
    </main>
  )
}

Step 4: Creating Additional Pages

Next.js 16 uses file-system based routing. Let's create our About and Categories pages.

Create app/about/page.tsx:

export default function About() {
  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold">AboutPage</h1>
    </main>
  )
}

Create app/categories/page.tsx:

export default function Categories() {
  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold">CategoriesPage</h1>
    </main>
  )
}

Step 5: Setting Up Dark Mode

Dark mode is a must-have feature for modern applications. We'll use next-themes, which handles everything seamlessly—no flash of unstyled content, system preference detection, and persistence.

Install Dependencies

npm install next-themes
npx shadcn@latest add dropdown-menu
npx shadcn@latest add button

Create Theme Provider

Create app/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>
}

The "use client" directive is crucial because next-themes needs to run on the client side to detect and apply themes.

Update Root Layout

Modify app/layout.tsx to include the ThemeProvider:

import type { Metadata } from "next";
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: "Tech Blog",
  description: "A modern blog built with Next.js",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={inter.className}>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Important: Notice the suppressHydrationWarning attribute on the <html> tag. This prevents hydration warnings that occur because the theme is determined on the client side.

Step 6: Building the Theme Toggle Component

Create app/components/theme-toggle.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 ThemeToggle() {
  const { setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <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>
  )
}

This component features:

  • Smooth icon transitions between light and dark modes
  • Three theme options: Light, Dark, and System
  • Accessible design with screen reader support
  • shadcn/ui components for consistent styling

Step 7: Creating the Header Component

Create app/components/header.tsx:

import Link from "next/link"
import { ThemeToggle } from "./theme-toggle"

export function Header() {
  return (
    <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
      <div className="container flex h-16 items-center justify-between">
        <div className="flex items-center gap-8">
          <Link href="/" className="flex items-center space-x-2">
            <span className="text-xl font-bold">Tech Blog</span>
          </Link>
          <nav className="flex items-center gap-6">
            <Link
              href="/"
              className="text-sm font-medium transition-colors hover:text-primary"
            >
              Home
            </Link>
            <Link
              href="/categories"
              className="text-sm font-medium transition-colors hover:text-primary"
            >
              Categories
            </Link>
            <Link
              href="/about"
              className="text-sm font-medium transition-colors hover:text-primary"
            >
              About
            </Link>
          </nav>
        </div>
        <ThemeToggle />
      </div>
    </header>
  )
}

Key features of our header:

  • Sticky positioning: Stays at the top when scrolling
  • Backdrop blur: Modern glassmorphism effect
  • Responsive design: Uses Tailwind's container utility
  • Smooth transitions: Hover effects on navigation links
  • Next.js Link: Client-side navigation for instant page changes

Step 8: Adding Header to Layout

Update app/layout.tsx to include the Header:

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "./components/theme-provider";
import { Header } from "./components/header";

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

export const metadata: Metadata = {
  title: "Tech Blog",
  description: "A modern blog built with Next.js",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={inter.className}>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <Header />
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Step 9: Testing Your Application

Start the development server:

npm run dev

Open your browser and navigate to http://localhost:3000. You should see:

  1. A clean header with your blog title
  2. Navigation links to Home, Categories, and About
  3. A theme toggle button in the top right
  4. Smooth transitions when switching between pages
  5. Perfect dark mode functionality with persistence

Try clicking between pages—notice how fast the navigation is? That's Next.js's client-side routing in action. Now toggle the theme and refresh the page. Your theme preference persists!

Common Issues and Solutions

Hydration Error

If you see a hydration mismatch warning about the <html> element, make sure you've added suppressHydrationWarning to your <html> tag in layout.tsx. This is expected behavior with theme providers and is the recommended solution.

Theme Flash on Page Load

If you see a brief flash of the wrong theme, ensure you've set disableTransitionOnChange in your ThemeProvider props. This prevents unwanted transitions during page load.

What's Next?

Congratulations! You've successfully set up a modern Next.js blog application with dark mode. In Part 2 of this series, we'll:

  • Design a beautiful hero section
  • Create blog post card components
  • Implement a responsive grid layout
  • Add more Tailwind CSS customizations
  • Set up a mock blog post data structure

Key Takeaways

In this tutorial, you learned:

  • How to set up Next.js 16 with TypeScript and Tailwind CSS
  • How to configure and use shadcn/ui components
  • How to implement dark mode with next-themes
  • How to create a responsive header with navigation
  • How to handle common hydration issues in Next.js

The combination of Next.js, Tailwind CSS, and shadcn/ui provides an excellent foundation for building modern web applications. The setup we've created is production-ready, accessible, and performant.

Resources

Have questions or suggestions? Feel free to leave a comment below! Stay tuned for Part 2 where we'll start building out the actual blog functionality.

Happy coding! 🚀

025

Comments (0)

Sign in to join the conversation