Modern Tailwind CSS and Next.js setup

The Tailwind setup I use now is boring, which is a compliment.
Older Next.js + Tailwind projects often needed Babel plugins, purge config, custom webpack fixes, or CSS-in-JS bridges. The current path is much cleaner: start with the App Router, keep styles global where they belong, and let Tailwind scan the files that actually contain UI.
Start the project
pnpm create next-app@latest my-tailwind-app --typescript --app
cd my-tailwind-appI keep TypeScript on by default. It is easier to remove strictness later than to retrofit it after components have spread across the app.
Add Tailwind
pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -pUse content globs that match the App Router and shared components:
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./app/**/*.{ts,tsx,mdx}',
'./components/**/*.{ts,tsx,mdx}',
'./pages/**/*.{ts,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [require('@tailwindcss/typography')],
}
export default configThe typography plugin is worth adding if the site has MDX, docs, or long-form content. It saves a lot of one-off prose styling.
Wire the stylesheet
Create app/globals.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
color-scheme: dark;
}
body {
@apply bg-black font-sans text-zinc-100;
}Then import it from the root layout and add fonts in one place:
import type { Metadata } from 'next'
import { Inter, JetBrains_Mono } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const mono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' })
export const metadata: Metadata = {
title: 'My Tailwind Starter',
description: 'A small Next.js and Tailwind starter.',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={`${inter.variable} ${mono.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}Keep the first page simple
export default function HomePage() {
return (
<main className="mx-auto flex min-h-screen max-w-3xl flex-col gap-8 px-6 py-16">
<section className="space-y-3">
<h1 className="text-4xl font-semibold">
Modern Tailwind CSS with Next.js
</h1>
<p className="text-lg text-zinc-400">
A small starter with App Router, TypeScript, Tailwind, and typography
styles.
</p>
</section>
<article className="prose prose-invert">
<h2>Why this setup holds up</h2>
<p>
There is no custom Babel config, no CSS-in-JS bridge, and no hidden
webpack patch. Tailwind scans the files you edit, PostCSS handles the
transform, and the App Router keeps route code close to the UI.
</p>
</article>
</main>
)
}That is enough. Add design tokens once the product has a real visual system. Until then, a small Tailwind setup beats a clever one.
A few defaults I keep
I usually add the small decisions below before the first real screen lands:
- one font stack for body text and one for code,
- a small set of semantic colors in CSS variables,
- typography styles for MDX or CMS content,
- a
cnortwMergehelper for conditional classes, - strict image sizing for cards, avatars, logos, and hero media.
The last point matters more than it sounds. Most messy Tailwind pages are not messy because Tailwind failed. They are messy because images, cards, buttons, and nav items do not have stable dimensions. A hover state changes height. A long title pushes a sibling card down. An icon has a different optical size from the next one.
Tailwind is good at fixing that if the component author is explicit. Use
aspect-ratio for media, fixed square dimensions for icon buttons, min-height
for repeated cards, and constrained line lengths for prose. The utilities are
not the design system. They are just the vocabulary.