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