Web Engineering

Tailwind 4 made CSS configuration feel like CSS

3 min read

Tailwind CSS v4 made configuration feel more like CSS, which is a bigger change than it sounds.

Older Tailwind projects often had a split brain. The visual decision lived in tailwind.config.js, the consuming code lived in class names, and the actual cascade lived somewhere else. That was workable, but it made styling feel like a tiny compiler project.

Version 4 pulls more of that work into CSS itself.

@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);
}

That changes the editing loop. The configuration still feeds the utility system, but the file now looks like the medium it controls.

two jobs used to share one file

Tailwind configuration has always carried two kinds of information.

The first kind is plumbing: content paths, plugins, presets, build details, compatibility knobs. That belongs in a config layer because it describes how the tool runs.

The second kind is design language: colors, type scales, breakpoints, shadows, spacing, easing, and named values. That is closer to CSS. Those decisions affect the cascade, inherit through components, and eventually become visual behavior in the browser.

Putting both jobs in a JavaScript config file made sense historically, but it created a weird reading experience. A designer reviewing a color decision had to leave CSS to understand the CSS.

css variables make the decision inspectable

@theme uses CSS custom properties, which gives the names a useful second life.

That means a token can be referenced directly, inspected in devtools, overridden in scoped contexts, and reasoned about with the rest of the cascade.

@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 is a small thing until a project has a lot of surfaces. Then it matters that a token is readable in the same place as the exception that uses it.

css-first does not mean config-free

I do not read v4 as a call to delete configuration discipline.

Large teams still need naming rules. They still need to decide whether a token is primitive, semantic, or component-specific. They still need migration plans when a color changes. They still need to decide whether a plugin belongs in the project or whether the project has grown a private dialect of Tailwind.

CSS-first configuration can also make a mess feel more official. A giant @theme block with hundreds of one-off values is still a junk drawer.

the useful question is meaning

The cleanup pass should ask what each theme value means.

@theme {
  --color-blue-500: oklch(0.62 0.19 255);
  --color-action-primary: var(--color-blue-500);
  --color-link: var(--color-blue-500);
}

Primitive values help with palettes. Semantic values help with product meaning. Mixing those layers casually is how a redesign turns into a scavenger hunt.

fewer files means fewer excuses

Moving configuration closer to CSS does remove one common excuse: “I did not know where that value came from.”

When a project has theme values in CSS, component overrides in CSS, and generated utilities reading from the same source, drift is easier to spot. A hardcoded border color or shadow becomes a visible decision during review instead of a hidden one.

That is the kind of friction I want from a styling system. It should make shortcuts visible without turning every adjustment into ceremony.

Tailwind v4 does not make styling simple. It does make styling decisions easier to inspect in the language the browser actually speaks at the end of the pipeline.

Jeremy London

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.