Modern Tailwind CSS and Next.js setup

Modern Tailwind CSS and Next.js setup cover image

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-app

I 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 -p

Use 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 config

The 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 cn or twMerge helper 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.