AiTechWorlds
AiTechWorlds
Tailwind CSS is included by default in create-next-app, and this lesson covers the configuration patterns, plugins, and techniques specific to using it in a Next.js application, beyond the utility-class basics you already know.
Think of Tailwind's plugins and theme config as pre-wired electrical outlets in a house — the typography plugin is the outlet already wired for a lamp (CMS or markdown content), while custom theme tokens are outlets wired to your own circuit (brand colors, fonts) instead of the generic default.
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.
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>
);
}// 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;// 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],
}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