|
| 1 | +--- |
| 2 | +name: tailwind-impl-build-nextjs |
| 3 | +description: > |
| 4 | + Use when installing or wiring Tailwind CSS into a Next.js project, for |
| 5 | + either the App Router or the Pages Router, on Tailwind v3 or v4, and when |
| 6 | + hooking next/font/google families into the Tailwind theme so utilities |
| 7 | + like font-sans resolve to the loaded family. Prevents the four most |
| 8 | + common Next-js install traps: importing globals.css in the wrong file |
| 9 | + for the active router, forgetting the @tailwindcss/postcss plugin under |
| 10 | + v4, breaking the v3 content glob on catch-all route folders named |
| 11 | + [...slug], and pasting font CSS variables into next/font/local without |
| 12 | + declaring them inside @theme inline so they never become utilities. |
| 13 | + Covers v4 App Router with @import tailwindcss, v4 Pages Router, v3 App |
| 14 | + Router with @tailwind directives, v3 Pages Router, next/font wiring via |
| 15 | + className on html plus @theme inline { --font-sans: var(--font-inter) }, |
| 16 | + why React Server Components have zero runtime cost from Tailwind, and |
| 17 | + the Turbopack arbitrary-value miss workaround for Next 16 plus v4 from |
| 18 | + tailwindlabs/tailwindcss issue 19825. |
| 19 | + Keywords: tailwind nextjs, tailwind next-js, install tailwind next, |
| 20 | + next app router tailwind, next pages router tailwind, app/globals-css, |
| 21 | + pages/_app-tsx, @tailwindcss/postcss, postcss-config-mjs, tailwind v4 |
| 22 | + next, tailwind v3 next, @import tailwindcss next, @tailwind directives |
| 23 | + next, tailwind init -p next, next/font google tailwind, next/font local |
| 24 | + tailwind, --font-inter @theme inline, font-sans next/font, RSC |
| 25 | + tailwind, server components tailwind, no runtime cost styling, |
| 26 | + turbopack arbitrary value miss, aspect-[12/5] missing turbopack, |
| 27 | + z-[100] missing, next 16 tailwind v4 issue 19825, catch-all route |
| 28 | + [...slug] glob, @source './[[]**[]]', how do I add tailwind to next, |
| 29 | + my tailwind classes are not applying in nextjs, blank styles next, |
| 30 | + why doesn't dark mode work next, getting started tailwind next. |
| 31 | +license: MIT |
| 32 | +compatibility: "Designed for Claude Code. Requires Tailwind CSS v3.4 or v4.0+." |
| 33 | +metadata: |
| 34 | + author: OpenAEC-Foundation |
| 35 | + version: "1.0" |
| 36 | +--- |
| 37 | + |
| 38 | +# Tailwind CSS in Next.js : Install and Wire-up |
| 39 | + |
| 40 | +Covers every supported combination of Tailwind version (v3.4 vs v4.0+) and |
| 41 | +Next.js router (App Router vs Pages Router), plus next/font integration |
| 42 | +and the documented Turbopack quirk. |
| 43 | + |
| 44 | +Companion skills : |
| 45 | + |
| 46 | +- `tailwind-impl-config-v4` : v4 CSS-first config surface (@theme, @source) |
| 47 | +- `tailwind-impl-config-v3` : v3 JS config surface (tailwind.config.js) |
| 48 | +- `tailwind-impl-build-vite` : same install topic for the Vite plugin |
| 49 | +- `tailwind-core-v3-vs-v4` : version-wide API differences |
| 50 | + |
| 51 | +## Decision : Which Combination Do You Have |
| 52 | + |
| 53 | +| Tailwind version | Router | Entry CSS lives in | Imported from | |
| 54 | +|------------------|--------|--------------------|---------------| |
| 55 | +| v4 | App Router | `app/globals.css` | `app/layout.tsx` | |
| 56 | +| v4 | Pages Router | `styles/globals.css` | `pages/_app.tsx` | |
| 57 | +| v3 | App Router | `app/globals.css` | `app/layout.tsx` | |
| 58 | +| v3 | Pages Router | `styles/globals.css` | `pages/_app.tsx` | |
| 59 | + |
| 60 | +The CSS file location is identical across versions. Only the file |
| 61 | +contents differ : `@import "tailwindcss";` for v4, three `@tailwind` |
| 62 | +directives for v3. |
| 63 | + |
| 64 | +## Minimum Setup : Tailwind v4 + Next.js App Router |
| 65 | + |
| 66 | +```bash |
| 67 | +npm install tailwindcss @tailwindcss/postcss postcss |
| 68 | +``` |
| 69 | + |
| 70 | +`postcss.config.mjs` (project root) : |
| 71 | + |
| 72 | +```js |
| 73 | +const config = { |
| 74 | + plugins: { |
| 75 | + "@tailwindcss/postcss": {}, |
| 76 | + }, |
| 77 | +}; |
| 78 | +export default config; |
| 79 | +``` |
| 80 | + |
| 81 | +`app/globals.css` : |
| 82 | + |
| 83 | +```css |
| 84 | +@import "tailwindcss"; |
| 85 | +``` |
| 86 | + |
| 87 | +`app/layout.tsx` : |
| 88 | + |
| 89 | +```tsx |
| 90 | +import "./globals.css"; |
| 91 | + |
| 92 | +export default function RootLayout({ children }: { children: React.ReactNode }) { |
| 93 | + return ( |
| 94 | + <html lang="en"> |
| 95 | + <body>{children}</body> |
| 96 | + </html> |
| 97 | + ); |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +That is the complete v4 + App Router install. No `tailwind.config.js`, no |
| 102 | +`@tailwind` directives, no `autoprefixer`, no `postcss-import`. The |
| 103 | +`@tailwindcss/postcss` plugin runs the Oxide engine which bundles all |
| 104 | +three. |
| 105 | + |
| 106 | +## Minimum Setup : Tailwind v4 + Next.js Pages Router |
| 107 | + |
| 108 | +Install and PostCSS config are identical to the App Router setup above. |
| 109 | +Only the import location moves : |
| 110 | + |
| 111 | +`styles/globals.css` : |
| 112 | + |
| 113 | +```css |
| 114 | +@import "tailwindcss"; |
| 115 | +``` |
| 116 | + |
| 117 | +`pages/_app.tsx` : |
| 118 | + |
| 119 | +```tsx |
| 120 | +import "@/styles/globals.css"; |
| 121 | +import type { AppProps } from "next/app"; |
| 122 | + |
| 123 | +export default function App({ Component, pageProps }: AppProps) { |
| 124 | + return <Component {...pageProps} />; |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +## Minimum Setup : Tailwind v3 + Next.js App Router |
| 129 | + |
| 130 | +```bash |
| 131 | +npm install -D tailwindcss@3 postcss autoprefixer |
| 132 | +npx tailwindcss init -p |
| 133 | +``` |
| 134 | + |
| 135 | +`tailwindcss init -p` generates two files : |
| 136 | + |
| 137 | +`tailwind.config.js` : |
| 138 | + |
| 139 | +```js |
| 140 | +/** @type {import('tailwindcss').Config} */ |
| 141 | +module.exports = { |
| 142 | + content: [ |
| 143 | + "./app/**/*.{js,ts,jsx,tsx,mdx}", |
| 144 | + "./pages/**/*.{js,ts,jsx,tsx,mdx}", |
| 145 | + "./components/**/*.{js,ts,jsx,tsx,mdx}", |
| 146 | + "./src/**/*.{js,ts,jsx,tsx,mdx}", |
| 147 | + ], |
| 148 | + theme: { extend: {} }, |
| 149 | + plugins: [], |
| 150 | +}; |
| 151 | +``` |
| 152 | + |
| 153 | +`postcss.config.js` : |
| 154 | + |
| 155 | +```js |
| 156 | +module.exports = { |
| 157 | + plugins: { |
| 158 | + tailwindcss: {}, |
| 159 | + autoprefixer: {}, |
| 160 | + }, |
| 161 | +}; |
| 162 | +``` |
| 163 | + |
| 164 | +`app/globals.css` : |
| 165 | + |
| 166 | +```css |
| 167 | +@tailwind base; |
| 168 | +@tailwind components; |
| 169 | +@tailwind utilities; |
| 170 | +``` |
| 171 | + |
| 172 | +`app/layout.tsx` : identical to the v4 case, just `import "./globals.css";`. |
| 173 | + |
| 174 | +## Minimum Setup : Tailwind v3 + Next.js Pages Router |
| 175 | + |
| 176 | +Same install, same `tailwind.config.js`, same `postcss.config.js`. The CSS |
| 177 | +file lives at `styles/globals.css` (same three `@tailwind` directives), |
| 178 | +imported once in `pages/_app.tsx` : |
| 179 | + |
| 180 | +```tsx |
| 181 | +import "@/styles/globals.css"; |
| 182 | +import type { AppProps } from "next/app"; |
| 183 | + |
| 184 | +export default function App({ Component, pageProps }: AppProps) { |
| 185 | + return <Component {...pageProps} />; |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +## Wire next/font/google Into the Tailwind Theme |
| 190 | + |
| 191 | +next/font emits a CSS variable that you opt into by applying a className. |
| 192 | +Tailwind reads that variable as a token via `@theme` (v4) or |
| 193 | +`fontFamily.extend` (v3). |
| 194 | + |
| 195 | +### v4 pattern |
| 196 | + |
| 197 | +`app/layout.tsx` : |
| 198 | + |
| 199 | +```tsx |
| 200 | +import { Inter } from "next/font/google"; |
| 201 | +import "./globals.css"; |
| 202 | + |
| 203 | +const inter = Inter({ |
| 204 | + subsets: ["latin"], |
| 205 | + variable: "--font-inter", |
| 206 | +}); |
| 207 | + |
| 208 | +export default function RootLayout({ children }: { children: React.ReactNode }) { |
| 209 | + return ( |
| 210 | + <html lang="en" className={inter.variable}> |
| 211 | + <body className="font-sans">{children}</body> |
| 212 | + </html> |
| 213 | + ); |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +`app/globals.css` : |
| 218 | + |
| 219 | +```css |
| 220 | +@import "tailwindcss"; |
| 221 | + |
| 222 | +@theme inline { |
| 223 | + --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif; |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +ALWAYS use `@theme inline { ... }` (not bare `@theme { ... }`) when the |
| 228 | +token value contains a `var(...)` reference to another CSS variable that |
| 229 | +exists at runtime. Bare `@theme` resolves the variable at build time and |
| 230 | +emits the resolved literal, which is empty before next/font runs and |
| 231 | +strips the family. |
| 232 | + |
| 233 | +### v3 pattern |
| 234 | + |
| 235 | +`pages/_app.tsx` (or `app/layout.tsx`) wires the className the same way. |
| 236 | +The config file declares the family : |
| 237 | + |
| 238 | +```js |
| 239 | +// tailwind.config.js |
| 240 | +module.exports = { |
| 241 | + content: [/* unchanged */], |
| 242 | + theme: { |
| 243 | + extend: { |
| 244 | + fontFamily: { |
| 245 | + sans: ["var(--font-inter)", "ui-sans-serif", "system-ui", "sans-serif"], |
| 246 | + }, |
| 247 | + }, |
| 248 | + }, |
| 249 | +}; |
| 250 | +``` |
| 251 | + |
| 252 | +## React Server Components : Zero Runtime Cost |
| 253 | + |
| 254 | +Tailwind is a build-time compiler. It scans source files for class |
| 255 | +tokens, generates a single static CSS bundle, and injects that bundle via |
| 256 | +the same `<link rel="stylesheet">` Next.js uses for every other CSS |
| 257 | +import. There is no client runtime, no Server Component restriction, no |
| 258 | +`"use client"` boundary needed for styling. RSC, Client Components, and |
| 259 | +Server Actions all consume the same CSS. |
| 260 | + |
| 261 | +Consequences : |
| 262 | + |
| 263 | +- NEVER move a Server Component to Client just to use Tailwind classes. |
| 264 | +- ALWAYS apply Tailwind classes directly on RSC output ; no wrapper. |
| 265 | +- Bundle size scales with the number of utilities used, not with the |
| 266 | + number of components. Adding a hundred RSC components that share the |
| 267 | + same utilities adds zero CSS bytes. |
| 268 | + |
| 269 | +## Turbopack Arbitrary-Value Miss (Issue 19825) |
| 270 | + |
| 271 | +Affects Next.js 16.1.6 + Tailwind v4.2.1 + `next dev --turbo` (and |
| 272 | +`next dev` once Turbopack becomes the default). Classes with arbitrary |
| 273 | +square-bracket values render in the DOM but the corresponding CSS rule |
| 274 | +is missing from the injected stylesheet. |
| 275 | + |
| 276 | +Reproduced tokens : `aspect-[12/5]`, `z-[100]`, `h-[80vh]`. The same |
| 277 | +classes compile correctly under webpack (`next dev` without `--turbo`). |
| 278 | + |
| 279 | +### Workarounds |
| 280 | + |
| 281 | +1. **Inline style for layout-critical values** : |
| 282 | + ```tsx |
| 283 | + <div style={{ aspectRatio: "12/5" }} className="w-full bg-zinc-900" /> |
| 284 | + ``` |
| 285 | +2. **Safelist the exact token via `@source inline`** so the scanner is |
| 286 | + forced to emit the rule regardless of Turbopack's incremental scan : |
| 287 | + ```css |
| 288 | + @source inline("aspect-[12/5] z-[100] h-[80vh]"); |
| 289 | + ``` |
| 290 | +3. **Disable Turbopack for the affected build** : remove `--turbo` from |
| 291 | + `next dev` until the upstream fix lands. |
| 292 | + |
| 293 | +NEVER ship `aspect-[12/5]` style arbitrary values to a Turbopack |
| 294 | +production build without first verifying the rule appears in |
| 295 | +`.next/static/css`. The DOM showing the class is not proof the CSS |
| 296 | +exists. |
| 297 | + |
| 298 | +## Catch-All Route Glob Trap (Tailwind v3) |
| 299 | + |
| 300 | +The default v3 content glob `./app/**/*.{js,ts,jsx,tsx,mdx}` treats |
| 301 | +square brackets as a character class. Files inside a folder named |
| 302 | +`[...slug]` or `[id]` are silently excluded from the scan. Symptom : |
| 303 | +classes on dynamic-route pages produce no CSS. |
| 304 | + |
| 305 | +Fix (v3) : add a second content entry that escapes the brackets, or |
| 306 | +list the catch-all folder explicitly : |
| 307 | + |
| 308 | +```js |
| 309 | +content: [ |
| 310 | + "./app/**/*.{js,ts,jsx,tsx,mdx}", |
| 311 | + "./app/[[]**[]]/*.{js,ts,jsx,tsx,mdx}", |
| 312 | +], |
| 313 | +``` |
| 314 | + |
| 315 | +Fix (v4) : add an explicit `@source` : |
| 316 | + |
| 317 | +```css |
| 318 | +@import "tailwindcss"; |
| 319 | +@source "./app/[[]**[]]/**/*.{js,ts,jsx,tsx,mdx}"; |
| 320 | +``` |
| 321 | + |
| 322 | +## Verification Checklist |
| 323 | + |
| 324 | +1. CSS file imports exactly once (root layout for App Router, `_app.tsx` |
| 325 | + for Pages Router). Importing twice doubles the bundle. |
| 326 | +2. `npm run dev` shows utilities applying within one second on hot |
| 327 | + reload. |
| 328 | +3. Dynamic-route pages (any folder with `[id]`, `[...slug]`, |
| 329 | + `[[...slug]]`) render utilities correctly. If not, the bracket-glob |
| 330 | + trap is active. |
| 331 | +4. If next/font is wired : a token like `text-3xl` renders with the |
| 332 | + loaded family, not the browser fallback. Inspect computed |
| 333 | + `font-family` in DevTools. |
| 334 | +5. Production : `npm run build` finishes without `PostCSS plugin |
| 335 | + tailwindcss requires PostCSS 8` (v3 only ; install `postcss@8`). |
| 336 | +6. Turbopack : if `next dev --turbo`, confirm arbitrary-value classes |
| 337 | + actually emit CSS, not just DOM attributes. |
| 338 | + |
| 339 | +## References |
| 340 | + |
| 341 | +- `references/methods.md` : full per-router install walkthroughs with |
| 342 | + every file and exact contents |
| 343 | +- `references/examples.md` : working `app/layout.tsx`, `pages/_app.tsx`, |
| 344 | + fonts wiring, mixed-mode setups |
| 345 | +- `references/anti-patterns.md` : the eight Next.js-specific traps with |
| 346 | + symptoms, root cause, fix, and verification |
| 347 | + |
| 348 | +## Sources |
| 349 | + |
| 350 | +- https://tailwindcss.com/docs/installation/framework-guides/nextjs |
| 351 | +- https://v3.tailwindcss.com/docs/guides/nextjs |
| 352 | +- https://github.com/tailwindlabs/tailwindcss/issues/19825 |
| 353 | +- https://github.com/tailwindlabs/tailwindcss/issues/16287 |
| 354 | +- https://nextjs.org/docs/app/api-reference/components/font |
0 commit comments