h6nk
← Back to writing

Tailwind v4: What Actually Changed and Why It's Better for Agentic Code Generation

March 27, 2026·8 min read·engineeringcssnextjsagentic-engineering

Tailwind v4 is out and most of the writing about it falls into one of two categories: the official migration guide (accurate but dry) or takes from people who haven't shipped it yet (speculation dressed as opinion). This post is neither. It's what I actually encountered migrating this portfolio from v3 to v4, and why the new model is meaningfully better — especially if you're working with agentic code generation.

The short version: Tailwind v4 removes the JavaScript config file entirely. Configuration is now done in CSS. That sounds like a minor implementation detail. It isn't.

What Tailwind v3 Actually Looked Like

If you've been using Tailwind for a while, the setup is automatic at this point. You have a tailwind.config.js (or .ts) that looks roughly like this:

// tailwind.config.js — v3 pattern
module.exports = {
  content: ["./src/**/*.{ts,tsx,js,jsx,mdx}"],
  theme: {
    extend: {
      colors: {
        accent: "#6366f1",
        "accent-hover": "#818cf8",
        "bg-primary": "#0a0a0a",
        "bg-secondary": "#141414",
        "bg-tertiary": "#1a1a1a",
        "text-primary": "#fafafa",
        "text-secondary": "#a0a0a0",
        border: "#262626",
      },
      fontFamily: {
        sans: ["var(--font-inter)", "Inter", "system-ui", "sans-serif"],
        mono: ["var(--font-jetbrains-mono)", "monospace"],
      },
    },
  },
};

This generates utility classes like bg-accent, text-bg-primary, font-sans and so on. Standard, well-documented, works fine. Hundreds of tutorials cover exactly this pattern.

There's nothing wrong with it. But there's something subtle happening that becomes important when you're using LLMs to generate code: the configuration lives in a JavaScript file that your CSS doesn't know about directly. Tailwind reads the config at build time, generates the utilities, and they appear in your classes. The connection between your intent (I want an indigo accent color) and the output (bg-accent works) passes through a build pipeline that doesn't surface in the file where you actually use those classes.

The v4 Change: Configuration Moves Into CSS

In v4, the config file is gone. Everything goes into your CSS via two directives: @import "tailwindcss" and @theme.

Here's what this portfolio's actual globals.css looks like:

@import "tailwindcss";
 
:root {
  --bg-primary: #0a0a0a;
  --bg-secondary: #141414;
  --bg-tertiary: #1a1a1a;
  --text-primary: #fafafa;
  --text-secondary: #a0a0a0;
  --accent: #6366f1;
  --accent-hover: #818cf8;
  --border: #262626;
}
 
@theme inline {
  --color-bg-primary: var(--bg-primary);
  --color-bg-secondary: var(--bg-secondary);
  --color-bg-tertiary: var(--bg-tertiary);
  --color-text-primary: var(--text-primary);
  --color-text-secondary: var(--text-secondary);
  --color-accent: var(--accent);
  --color-accent-hover: var(--accent-hover);
  --color-border: var(--border);
  --font-sans: var(--font-inter);
  --font-mono: var(--font-jetbrains-mono);
}

The @import "tailwindcss" line replaces the old @tailwind base; @tailwind components; @tailwind utilities; triple. The @theme inline block defines your design tokens as CSS custom properties using a naming convention that Tailwind v4 understands.

The inline keyword in @theme inline is important: it tells Tailwind to use the resolved CSS variable values rather than emitting separate theme variables. This means your dark mode token --bg-primary: #0a0a0a gets used directly rather than generating a second layer of --tw-color-* variables.

After this, bg-bg-primary, text-text-primary, and text-accent all work as utility classes — generated directly from the CSS custom properties in @theme.

What You Actually Have to Change

If you're migrating a v3 project, here's where the real work is:

Delete tailwind.config.js (or .ts). It no longer does anything. Tailwind v4's PostCSS plugin doesn't read it. If you leave it, it's just a file sitting there lying to future readers.

Replace your three @tailwind directives with one @import "tailwindcss". This goes at the top of your root CSS file.

Move your theme customizations into @theme {}. The naming convention for colors is --color-{name}, for fonts it's --font-{name}, for spacing --spacing-{name}. Tailwind derives the utility classes from these.

Update your utility class names if they changed. Some v3 utilities were renamed. The one that bit me: text-opacity-* is now handled differently via text-{color}/{opacity} syntax. Audit your codebase with a search for classes that might have shifted.

Check your content configuration. In v3 you specified content paths in the config. In v4, Tailwind automatically detects files in your project. If you have unusual paths or want to explicitly include something, use @source in your CSS instead.

The migration on this portfolio took about an hour end-to-end. The main time sink was auditing class names that behaved differently rather than the configuration change itself.

Why This Is Better for Agentic Code Generation

This is the part that doesn't appear in the official migration guide.

When I'm generating component code with an LLM — or when an autopilot agent is implementing a feature — the model needs to know what utility classes are available. In a v3 project, that information lives in tailwind.config.js. The model has to reason about a separate configuration file to know that bg-accent is valid, that font-sans maps to Inter, that bg-bg-tertiary exists.

This means one of three things happens:

  1. The model has seen enough Tailwind v3 projects to make a good guess (works most of the time, wrong in subtle ways when your custom config diverges from convention)
  2. You include the config file in context explicitly (works, but adds tokens and doesn't help with inline editing)
  3. The model hallucinates a class that doesn't exist in your specific project (common, annoying to debug)

With v4, the design system is in the CSS file. globals.css is the config. If the model can read the CSS — and it can, because it's just a file — it knows exactly what classes are available. There's no separate artifact to reason about.

Here's what that looks like in practice. When an agent is implementing a new component for this portfolio, the relevant part of the context is:

@theme inline {
  --color-accent: var(--accent);       /* → bg-accent, text-accent, border-accent */
  --color-bg-secondary: var(--bg-secondary);  /* → bg-bg-secondary */
  --font-mono: var(--font-jetbrains-mono);    /* → font-mono */
}

The mapping from theme variable to utility class is explicit and mechanical. The model doesn't need to infer anything about your config — it reads the CSS and the classes follow directly.

This also helps with design token consistency. In v3, there's always some drift risk: a component uses a raw hex value instead of the utility class, or references a token name slightly differently. With the CSS variables visible in the same file as the custom properties, the correct class is more obvious.

The @theme inline vs @theme Distinction

One thing that's underexplained in the documentation: @theme without inline and @theme inline behave differently.

@theme (without inline) tells Tailwind to generate its own --tw-color-* CSS variables alongside the utilities. You end up with both your custom variables and a Tailwind-generated layer.

@theme inline tells Tailwind to use your CSS custom properties directly as the theme values, without generating a separate variable layer. This is what you want when your design tokens are already defined as :root variables — you're pointing at them rather than duplicating them.

For a portfolio using a single-layer dark theme with CSS variables, inline is correct. For a project that needs to support Tailwind's built-in dark: variant with separate light/dark token sets, the choice is more nuanced.

What Didn't Change

The utility class mental model is the same. flex items-center justify-between still works exactly as it did in v3. Responsive prefixes (md:, lg:), hover states (hover:), and arbitrary values (w-[342px]) all work the same way.

The big productivity win of Tailwind — not switching between files to add styles — is unchanged. If anything, it's slightly better: you can put a @layer block right in your CSS file next to your theme definition when you need something that doesn't fit a utility pattern.

Honest Assessment

The migration is real work if your config is complex. Custom plugins, theme() function calls in your CSS, and projects that rely on specific Tailwind internals will have rough edges. The documentation on some edge cases (like @source and @plugin) is thinner than it should be.

But for a project that's using Tailwind for what it's designed for — a consistent set of design tokens, responsive utilities, and component-level styling — v4 is a clean improvement. The configuration is in CSS, where it belongs. The build pipeline is simpler. And for anyone generating code with language models, the design system being self-contained in a CSS file is a genuine win.

If you're starting a new Next.js project, use v4. If you're on v3 and it's working, the migration is worth doing but not urgent. The patterns you learn in v4 are where Tailwind is going — the CSS-first model is a better foundation.

← Back to writing