Tailwind v4 changes where a styling decision is legible.
In an older project, I might need to look in three places to answer a simple question: where did this color come from? A JavaScript config names the theme value, a class name consumes it, and the browser eventually resolves the result through the cascade. The system works, but the explanation is split across tool vocabulary and browser vocabulary.
More of that explanation now fits in CSS.
@import "tailwindcss";
@theme {
--font-display: "Inter", "sans-serif";
--breakpoint-wide: 1440px;
--color-panel: oklch(0.98 0.01 250);
--color-panel-ink: oklch(0.22 0.03 250);
--ease-soft: cubic-bezier(0.2, 0, 0, 1);
}
The benefit is not that configuration disappears. It is that visual configuration looks like the thing it configures.
two kinds of configuration
A Tailwind project has operational settings and design decisions.
Content paths, plugins, presets, and compatibility switches describe how the tool runs. JavaScript configuration is a reasonable home for those.
Colors, type scales, breakpoints, shadows, spacing, and easing describe the visual language. Those values eventually participate in CSS behavior. Keeping them in CSS makes the relationship easier to inspect during a review or in devtools.
That distinction gives a migration a useful boundary: move the design vocabulary closer to CSS without pretending the build system no longer needs configuration.
the cascade becomes part of the token story
CSS custom properties are useful beyond their syntax.
A named value can be read in a stylesheet, inspected in devtools, overridden in a scoped context, and traced through the cascade. The same name can connect a theme definition to the component that depends on it.
@theme {
--color-callout-bg: oklch(0.97 0.03 210);
--color-callout-border: oklch(0.72 0.08 210);
}
.docs-callout {
background: var(--color-callout-bg);
border-color: var(--color-callout-border);
}
That trace matters once a project has enough surfaces for visual drift to become normal. An exception is easier to question when it sits beside the rule it is overriding.
CSS-first still needs judgment
Putting values in CSS does not make them good values.
A large theme block can become a junk drawer just as easily as a JavaScript object can. Teams still need naming rules, a distinction between primitive and semantic values, and a decision about which values are stable enough to expose as shared utilities. Plugins can still grow a private dialect that new contributors have to learn.
The file location changes the reading experience. It does not remove design-system maintenance.
name the meaning, not only the hue
A useful cleanup separates palette values from product roles.
@theme {
--color-blue-500: oklch(0.62 0.19 255);
--color-action-primary: var(--color-blue-500);
--color-link: var(--color-blue-500);
}
The primitive says what the value is. The semantic names say why the product uses it. When a redesign changes the blue, the role names remain useful. When a role changes independently, the dependency is visible instead of hidden in a copied hex value.
the smaller win is inspectability
The strongest case for this direction is ordinary maintenance.
A developer can follow a token from its definition to the generated utility and into the rendered element. A reviewer can see a hardcoded border color or shadow as a decision instead of treating it as unexplained implementation detail. A designer can read more of the visual system without first translating it out of a build tool’s config format.
Tailwind v4 does not make styling simple. It makes more of the styling system readable in the language the browser actually computes. That is a modest change with a practical payoff: fewer places to search before deciding whether a value belongs in the system at all.
Related posts

About Jeremy London
Engineering leader and builder in Denver. I write about AI platforms, agents, security, reliability, homelab infrastructure, and the parts of engineering work that have to survive production.