Tailwind v4: What Actually Changed and Why It's Better for Agentic Code Generation
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:
- 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)
- You include the config file in context explicitly (works, but adds tokens and doesn't help with inline editing)
- 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.