Styling at Speed: Tailwind v4 With an Agent
Tailwind v4 moved config into CSS, and that made an agent a far better collaborator. How design tokens in one @theme block kept the whole site consistent.

Styling is where AI collaboration usually falls apart. Ask an agent to "make it look nicer" and you get a snowstorm of arbitrary values — mt-[13px] here, a hex code there — that drift a little more with every edit. Tailwind v4 changed that for me by moving configuration into CSS, and a shared set of tokens turned the agent from a source of drift into a source of consistency. Part of the Building with AI series.
Config moved into CSS
Tailwind v4 is CSS-first. Instead of a JavaScript config file, the design system is declared in the stylesheet with an @theme block, and every token there becomes a utility class automatically. One import, one place to define the vocabulary:
1@import "tailwindcss";
2
3@theme {
4 --color-ink: #0b1f26;
5 --color-surface: #fbf7ef;
6 --color-accent: #e08a4e;
7 --font-sans: "Inter", system-ui, sans-serif;
8 --spacing-gutter: 1.5rem;
9}That highlighted --color-ink line isn't just a variable — it mints bg-ink, text-ink, and border-ink for free. The token is the API, so there's a single, obvious name for "the main text colour" and no reason for anyone, human or agent, to reach for a raw hex.
Tokens are guardrails the agent respects
Here's the shift that mattered: a named token is a rule the agent can follow, while an empty canvas is a rule it can't. When I ask for a callout styled to match the site, it reaches for bg-surface text-ink because those exist and mean something — not bg-[#fbf7ef], which would work today and rot tomorrow.
A design token is a decision with a name. Give the agent names and it stops inventing decisions.
The CLAUDE.md rule is simply: style with tokens, never arbitrary values. Combined with the theme block, that keeps forty components speaking one visual language even though a dozen of them were first drafted by an agent.
Components stay declarative
Because the tokens carry the design, the components stay thin and readable — the class list is the spec, so a review diff shows intent, not magic numbers:
1export function Card({ title, excerpt }: { title: string; excerpt: string }) {
2 return (
3 <article className="bg-surface text-ink rounded-2xl p-gutter shadow-sm">
4 <h3 className="text-xl font-semibold">{title}</h3>
5 <p className="mt-2 opacity-80">{excerpt}</p>
6 </article>
7 );
8}Changing the whole look is one block
The real dividend came the day I wanted the site warmer. I didn't hunt through components — I edited three lines in @theme, and every card, quote, and code caption moved together. That's only possible because nothing hard-coded a colour; everything referenced a token.
This is the same principle as the rest of the build: put the source of truth in one place and let everything downstream derive from it. The schema did it for content, the service layer did it for data, and @theme does it for design. An agent working against a single source of truth is fast and safe; an agent improvising against a blank file is neither.
Give styling a vocabulary and the agent stops guessing. Tokens turned "make it look nicer" from a drift generator into a one-word instruction with a predictable result. Next, the trickier visual problem I couldn't tokenize my way out of — Shipping Heroes Without a Designer.
