Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
16 minLesson 28 of 33
Styling & UI

Tailwind CSS in Next.js

Tailwind CSS in Next.js

Tailwind is included by default in create-next-app. This lesson covers the configuration patterns, plugins, and techniques specific to using Tailwind in a Next.js application — beyond the basics you already know.

The Typography Plugin

The most important Tailwind plugin for content-heavy Next.js apps. It styles HTML content you don't control (from a CMS, markdown, or database) with beautiful defaults:

npm install @tailwindcss/typography
// tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
    content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
    theme: { extend: {} },
    plugins: [require("@tailwindcss/typography")],
};

export default config;
// Apply the prose class to any container with rendered content
export default async function BlogPostPage({ params }: Props) {
    const post = await getPost(params.slug);
    
    return (
        <article className="max-w-3xl mx-auto px-4 py-12">
            <h1>{post.title}</h1>
            <div
                className="prose prose-lg prose-gray max-w-none
                           prose-headings:font-bold
                           prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline
                           prose-code:bg-gray-100 prose-code:px-1 prose-code:rounded
                           prose-pre:bg-gray-900"
                dangerouslySetInnerHTML={{ __html: post.htmlContent }}
            />
        </article>
    );
}

prose applies typographic styles to all children: headings, paragraphs, lists, blockquotes, code blocks, tables. The modifier classes (prose-lg, prose-gray) customize it.

Dark Mode with Tailwind in Next.js

Set up dark mode with class strategy:

// tailwind.config.ts
const config: Config = {
    darkMode: "class",   // Toggle dark mode via class on <html>
    // ...
};

The dark mode theme toggle:

"use client";

import { useEffect, useState } from "react";

export function ThemeToggle() {
    const [isDark, setIsDark] = useState(false);
    
    useEffect(() => {
        const saved = localStorage.getItem("theme");
        const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
        const dark = saved === "dark" || (!saved && prefersDark);
        setIsDark(dark);
        document.documentElement.classList.toggle("dark", dark);
    }, []);
    
    function toggle() {
        const next = !isDark;
        setIsDark(next);
        document.documentElement.classList.toggle("dark", next);
        localStorage.setItem("theme", next ? "dark" : "light");
    }
    
    return (
        <button onClick={toggle} className="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
            {isDark ? "☀️" : "🌙"}
        </button>
    );
}

Prevent flash of wrong theme by setting class before React hydrates:

// src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="en" suppressHydrationWarning>
            <head>
                {/* Inline script runs before page renders */}
                <script
                    dangerouslySetInnerHTML={{
                        __html: `
                            try {
                                if (localStorage.theme === 'dark' || 
                                    (!localStorage.theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
                                    document.documentElement.classList.add('dark')
                                }
                            } catch {}
                        `,
                    }}
                />
            </head>
            <body>{children}</body>
        </html>
    );
}

Custom Theme Configuration

// tailwind.config.ts
import type { Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";

const config: Config = {
    darkMode: "class",
    content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
    theme: {
        extend: {
            fontFamily: {
                sans: ["var(--font-geist-sans)", ...fontFamily.sans],
                mono: ["var(--font-geist-mono)", ...fontFamily.mono],
            },
            colors: {
                brand: {
                    50:  "hsl(220, 100%, 97%)",
                    500: "hsl(220, 90%, 56%)",
                    600: "hsl(220, 90%, 48%)",
                    900: "hsl(220, 60%, 20%)",
                },
            },
            borderRadius: {
                "4xl": "2rem",
            },
            animation: {
                "fade-in": "fadeIn 0.2s ease-in-out",
                "slide-up": "slideUp 0.3s ease-out",
            },
            keyframes: {
                fadeIn: {
                    "0%": { opacity: "0" },
                    "100%": { opacity: "1" },
                },
                slideUp: {
                    "0%": { transform: "translateY(10px)", opacity: "0" },
                    "100%": { transform: "translateY(0)", opacity: "1" },
                },
            },
        },
    },
    plugins: [
        require("@tailwindcss/typography"),
        require("@tailwindcss/forms"),
    ],
};

export default config;

Using Google Fonts with Tailwind

// src/app/layout.tsx
import { Inter, Fira_Code } from "next/font/google";

const inter = Inter({
    subsets: ["latin"],
    variable: "--font-inter",   // CSS variable
});

const firaCode = Fira_Code({
    subsets: ["latin"],
    variable: "--font-fira-code",
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="en" className={`${inter.variable} ${firaCode.variable}`}>
            <body className="font-sans">{children}</body>
        </html>
    );
}
// tailwind.config.ts
fontFamily: {
    sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans],
    mono: ["var(--font-fira-code)", ...defaultTheme.fontFamily.mono],
}

CSS Variables with Tailwind

Use CSS variables for values that change dynamically (dark mode, user theme selection):

/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
    :root {
        --background: 0 0% 100%;
        --foreground: 222 47% 11%;
        --primary: 221 83% 53%;
        --primary-foreground: 0 0% 100%;
        --muted: 210 40% 96%;
        --muted-foreground: 215 20% 65%;
        --border: 214 32% 91%;
        --radius: 0.5rem;
    }
    
    .dark {
        --background: 222 47% 11%;
        --foreground: 210 40% 98%;
        --primary: 217 91% 60%;
        --muted: 217 32% 17%;
        --muted-foreground: 215 20% 65%;
        --border: 217 32% 17%;
    }
}
// tailwind.config.ts
colors: {
    background: "hsl(var(--background))",
    foreground: "hsl(var(--foreground))",
    primary: {
        DEFAULT: "hsl(var(--primary))",
        foreground: "hsl(var(--primary-foreground))",
    },
    muted: {
        DEFAULT: "hsl(var(--muted))",
        foreground: "hsl(var(--muted-foreground))",
    },
    border: "hsl(var(--border))",
},

This is exactly how Shadcn UI works — and now you can adopt or customize the same pattern.

Next lesson: Shadcn UI — the best component library for Next.js apps.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!