
Building a Next.js Blog with Tailwind CSS & ShadCN UI: Setup & Dark Mode
T
Tech & Beyond
December 11, 20257 min read
#next js#tailwind css#shadcn ui#react#typescript#dark mode#web development#frontend#tutorial#next themes
025
Comments (0)
Sign in to join the conversation
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:
Prerequisites
Before we begin, make sure you have:
Step 1: Creating the Next.js Application
Let's start by creating our Next.js application. Open your terminal and run:
Note: You can select the "Recommend Configuration"
You'll be prompted with several configuration options. Here's what to choose:
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:
Configuration options:
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.tsxand 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
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.tsxto 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
suppressHydrationWarningattribute 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:
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:
Step 8: Adding Header to Layout
Update
app/layout.tsxto 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:
Open your browser and navigate to
http://localhost:3000. You should see: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 addedsuppressHydrationWarningto your<html>tag inlayout.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
disableTransitionOnChangein 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:
Key Takeaways
In this tutorial, you learned:
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! 🚀