# Next.js 16 + Tailwind v4: 5 traps AI coding assistants keep hitting (and the prompts that fix them)

> Source: <https://dev.to/shipinanhour/nextjs-16-tailwind-v4-5-traps-ai-coding-assistants-keep-hitting-and-the-prompts-that-fix-them-3ioc>
> Published: 2026-10-19 04:08:16+00:00

A fresh `create-next-app` in late 2026 gives you Next.js 16 and Tailwind CSS v4. Most AI coding assistants learned on years of Next 13–15 and Tailwind v3 code. That gap produces the same handful of failures again and again, and they often show up only at `npm run build`, or only on a laptop set to dark mode.

Below are five of them. For each one you get the symptom (with the exact error text where there is one), why the assistant gets it wrong, the fix, and a short prompt you can paste into Cursor, Claude Code, or ChatGPT.

Versions used: `create-next-app` 16.4.2 (Next.js 16.4.2, React 19.3), Tailwind CSS 4.3, Node 20.

`npm run build` fails on a brand-new app (`cacheComponents`)
**Symptom.** `npm run dev` looks fine, then the build fails with one of these:

```
Route segment config "dynamic" is not compatible with `nextConfig.cacheComponents`. Please remove it.
Route "/expenses": Next.js encountered the unstable value `new Date()` in a Client Component.

This value would be evaluated during the prerender, instead of recomputed on each visit.
```

**Why the assistant gets it wrong.** With its recommended defaults (`--yes`), `create-next-app` 16.4 now writes this into `next.config.ts`:

``` js
const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
  turbopack: { /* … Tailwind CSS loader … */ },
};
```

The Next.js caching docs say it directly: new App Router apps made with the recommended defaults "already have Cache Components and Partial Prefetching enabled." The assistant doesn't know this. It reaches for the Next 13–15 habit of `export const dynamic = "force-dynamic"`, which Cache Components rejects, or it puts `new Date()` in a component that now gets prerendered.

In our test runs, both of our sample apps failed `npm run build` at this point. One failed on the `dynamic` export and one on a `new Date()` default for a date input. The fix that worked was removing the two config lines.

**Fix, option A: opt out** (fine for a localStorage app or a small landing page):

``` js
// next.config.ts
const nextConfig: NextConfig = {
  // removed: cacheComponents: true,
  // removed: partialPrefetching: true,
  turbopack: { /* keep this block: it loads Tailwind */ },
};
```

You can also scaffold without it. In our check, the 16.4.2 CLI accepted `--no-cache-components` even though `--help` doesn't list it:

```
npx create-next-app@latest my-app --ts --tailwind --eslint --app --use-npm --yes --no-cache-components
```

After either change, delete `.next/` and rebuild.

**Fix, option B: keep Cache Components and do it its way.** Remove the `dynamic` export. The migration guide says to drop `force-dynamic`, because uncached work already runs at request time. Then put the request-time part behind `<Suspense>` and `await connection()`:

``` js
import { Suspense } from "react";
import { connection } from "next/server";

async function SignupCount() {
  await connection(); // render at request time
  const total = await getTotal();
  return <p>{total} people joined</p>;
}

export default function Page() {
  return (
    <Suspense fallback={<p>…</p>}>
      <SignupCount />
    </Suspense>
  );
}
```

**Prompt:**

`npm run build` fails with "Route segment config "dynamic" is not compatible with nextConfig.cacheComponents" or "encountered the unstable value `new Date()`". This is Next.js 16, and `create-next-app` enabled `cacheComponents` by default. Open `next.config.ts`, remove `cacheComponents: true` and `partialPrefetching: true`, and keep the `turbopack` block. Make sure no component calls `new Date()`, `Date.now()`, or `Math.random()` while rendering. Delete `.next/` and re-run the build until it passes.

`new Date()` and `localStorage` during render (and the lint errors after you fix it)
**Symptom.** Hydration warnings. Data that flashes and disappears. A "today" date that is one day off. A default month that is frozen at whatever month you ran the build in. Once you fix those, `npm run lint` fails with `react-hooks/set-state-in-effect` or `react-hooks/purity`.

**Why the assistant gets it wrong.** Its go-to patterns are `useState(new Date())`, `useState(() => JSON.parse(localStorage.getItem(...)))`, and `new Date().toISOString().slice(0, 10)` for "today." All three either run during server prerendering or quietly use UTC. A common reflex is a `mounted` flag that renders "Loading…" until the effect runs. In our test runs that didn't help. The `useState` initializers still ran during prerender, and the build still failed.

Two more things from the test runs. A month default set during render ended up baked into the static HTML. Then the standard fix (empty state, load in `useEffect`) produced **10** `react-hooks/set-state-in-effect` errors under the ESLint config that Next 16 ships, which includes eslint-plugin-react-hooks 7.

**Fix.** Start with empty state and read browser values in an effect or an event handler. That is also the "[defer]" option Next's own error message suggests.

```
"use client";
const [expenses, setExpenses] = useState<Expense[]>([]);
const [date, setDate] = useState("");

useEffect(() => {
  setExpenses(loadExpenses()); // reads localStorage on the client only
  setDate(todayISO());
}, []);
```

Build "today" from local time, not `toISOString()`:

``` js
export function todayISO(): string {
  const d = new Date();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${d.getFullYear()}-${m}-${day}`;
}
```

For the lint rule you have two honest options. If loading from `localStorage` once on mount is intentional, turn off just that rule and leave a comment saying why:

```
// eslint.config.mjs
{
  // Loading from localStorage once on mount is intentional in this app.
  rules: { "react-hooks/set-state-in-effect": "off" },
},
```

Or design the effect away: derive the value, let CSS handle it (see Trap 4), or do the work in a handler. For `react-hooks/purity`, take `Date.now()` out of the component body and compute time-based values on the server, in a Route Handler or a server-only `lib/` helper.

House rule for this project: never read `localStorage` or call `new Date()`/` Date.now()` during render or in a `useState(...)` initial value. Start state empty (`[]` / `""`) and load it in `useEffect` or in a click/submit handler. Store dates as local `YYYY-MM-DD` and never use `toISOString()` for "today." A mounted-flag "Loading…" gate is not enough on its own. Then run `npm run lint` and `npm run build` and fix what they report.

**Symptom.** On a computer set to dark mode, the page is near-black with light text, and your new `bg-white` cards show light text on white. Every `border` looks dark.

**Why the assistant gets it wrong.** It builds on top of the template's `app/globals.css` without reading it. In 16.4.2 that file includes:

```
@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}
body {
  background: var(--background);
  color: var(--foreground);
}
```

On top of that, Tailwind v4 changed the default border color from `gray-200` to `currentColor` (see the upgrade guide's "Default border color" section). v3-trained assistants assume borders are a soft gray. In our test runs, both problems showed up in the final QA pass: the UI was unreadable on a dark-mode OS, and the borders defaulted to black.

**Fix.** Replace `globals.css` with a minimal version and set colors on `<body>` yourself:

```
@import "tailwindcss";

@layer base {
  *, ::before, ::after {
    border-color: var(--color-neutral-200);
  }
}
<body className="bg-neutral-50 text-neutral-900">
```

Text is unreadable on a computer set to dark mode, and all borders look black. In `app/globals.css`, remove the default `:root`, `prefers-color-scheme`, and `body` rules. Keep `@import "tailwindcss";` and add `@layer base { *, ::before, ::after { border-color: var(--color-neutral-200); } }`. Put `bg-neutral-50 text-neutral-900` on `<body>`. Tailwind only, no UI library.

**Symptom.** You click the toggle and nothing changes, or the page only goes dark when the OS is already dark. After a refresh you get a white flash.

**Why the assistant gets it wrong.** In Tailwind v3 you set `darkMode: "class"` in `tailwind.config.js`. In v4, `dark:` follows `prefers-color-scheme` by default, and class-based toggling is configured in CSS. Assistants still write the v3 config, or toggle the class on `<body>` instead of `<html>`. When we compiled Tailwind 4.3 without the override, `dark:` utilities came out inside a `prefers-color-scheme` media query, so a `.dark` class had no effect. Our budget-app test run hit exactly this: the toggle did nothing until we added `@custom-variant dark`.

**Fix.** This one line is from the Tailwind dark-mode docs:

```
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
```

Toggle the class on `document.documentElement`. Apply the saved theme before first paint with a small inline script, and let CSS pick the icon so there's no React state to mismatch on hydration:

``` js
// app/layout.tsx
const themeScript = `try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}`;

<html lang="en" suppressHydrationWarning>
  <head>
    <script dangerouslySetInnerHTML={{ __html: themeScript }} />
  </head>
  …
</html>
// components/ThemeToggle.tsx
"use client";
export default function ThemeToggle() {
  function toggle() {
    const isDark = document.documentElement.classList.toggle("dark");
    try { localStorage.setItem("theme", isDark ? "dark" : "light"); } catch {}
  }
  return (
    <button type="button" onClick={toggle} aria-label="Toggle dark mode">
      <span className="dark:hidden">🌙</span>
      <span className="hidden dark:inline">☀️</span>
    </button>
  );
}
```

The CSS-picked icon also avoids the `set-state-in-effect` lint error from Trap 2.

The dark-mode toggle does nothing. This is Tailwind v4: there is no `darkMode` setting in a JS config. Add `@custom-variant dark (&:where(.dark, .dark *));` to `globals.css`. Toggle the `dark` class on `document.documentElement`, not `<body>`. Add an inline `<head>` script that applies the saved theme before paint, and add `suppressHydrationWarning` on `<html>`. Choose the icon with `dark:hidden` / `hidden dark:inline` instead of React state. Then add missing `dark:` classes to cards, inputs, and the header.

**Symptom.** Shadows, corners, and blurs look slightly off. A custom brand color or gradient you asked for doesn't appear. The assistant creates a `tailwind.config.js` or adds `@tailwind base;` lines.

**Why the assistant gets it wrong.** Most of the Tailwind in its training data is v3. A common trap is a mix of v3 and v4 syntax:

| v3 habit | v4 | 
|---|---|
| `bg-gradient-to-r` | `bg-linear-to-r` (renamed in v4.0) | 
| `shadow-sm` /`shadow` | `shadow-xs` /`shadow-sm` | 
| `rounded-sm` /`rounded` | `rounded-xs` /`rounded-sm` | 
| `outline-none` | `outline-hidden` | 
| `ring` (3px) | `ring-3` | 
| `@tailwind base; …` | `@import "tailwindcss";` | 
| `tailwind.config.js` theme | `@theme { … }` in CSS (JS config only loads via`@config` ) | 

To be fair to `bg-gradient-to-r`: when we compiled it with Tailwind 4.3, it still produced CSS as a legacy alias. The v4 docs only use `bg-linear-*` (along with the new `bg-radial-*` and `bg-conic-*`), so standardize on that. A more common reason a gradient "doesn't show up" is that its colors are defined in a `tailwind.config.js` that v4 never loads, so classes like `from-brand` are never generated.

**Fix.** Define custom colors in CSS:

```
@import "tailwindcss";
@theme {
  --color-brand: #7c3aed;
}
<span className="bg-linear-to-r from-brand to-pink-600 bg-clip-text text-transparent">
  Ship it
</span>
```

This project uses Tailwind CSS v4, not v3. Don't create `tailwind.config.js` or use `@tailwind` directives. Theme values go in `@theme` in `globals.css`. Use v4 names: `bg-linear-to-*` instead of `bg-gradient-to-*`, `shadow-xs`/` rounded-xs` for the old `-sm` sizes, `outline-hidden` instead of `outline-none`. Gradient text needs `bg-clip-text text-transparent`. Search the codebase for v3 names and replace them.

`<a href="https://dev.to/#features">` triggers `@next/next/no-html-link-for-pages` in `npm run lint`. In one test run it produced 6 errors. Use `<Link href="https://dev.to/#features">` from `next/link`.` searchParams` is a Promise.`searchParams: Promise<Record<string, string | string[] | undefined>>` and `await` it. Assistants still destructure it synchronously.
Fixing these one at a time works, but the cheaper move is to stop the assistant from writing them in the first place. Put the house rules in a file your tool reads on every request: `AGENTS.md` at the project root (Cursor reads it, and so do recent Claude Code versions when there's no `CLAUDE.md`) or a `.cursor/rules/*.mdc` file with `alwaysApply: true`. `create-next-app` 16.4 already generates an `AGENTS.md` that tells agents to read the bundled docs in `node_modules/next/dist/docs/`. Add your rules below that block. Then make `npm run lint && npm run build` the final checkpoint of every session, because that is where most of these failures show up.

If you want these as ready-to-paste prompts, there's a free sample with four of them: the scaffold, the Tailwind v4 dark-mode toggle, and the build-failure and colors/gradients rescues. It's here: [https://shipinanhour.gumroad.com/l/qfzgeg](https://shipinanhour.gumroad.com/l/qfzgeg)
