Picture yourself inside a massive building. Could be an airport, could be a hospital, could be a museum. You're trying to reach the information desk, but there's not a single sign or marker anywhere.

Doors in red and blue, round windows and square ones, hallways lined with yellow trim, shadowed panels, neatly arranged seating, airy rooms with black-and-white tile floors... but nothing tells you what any of these places actually are.

Which one is the information desk? Which one is the exit? Which one is the conference room? Which one is the waiting lounge? Which one is the cafeteria? How would you find your way?

You'd find your way eventually. It's hard but not impossible. You'd open doors one by one. You'd ask people. You'd backtrack. You'd take the wrong elevator. You'd mutter "nope, not this one" a few times. Eventually, you'd get there.

But the point isn't whether you arrive. The point is how much effort it takes to get there.

This is exactly what AI agents experience today when working in projects built with Tailwind CSS and similar utility-based frameworks. Sure, the code runs, the components are in place, the pages render correctly. Utility classes make it perfectly clear how everything looks. But there's nothing telling you what each of those components actually is.

Let me make this concrete. You tell your AI agent something like:

"Make the heading in the contact section on the about page a bit bigger."

Your AI assistant springs into action, runs a grep "contact" in the background, and boom: four matches.

  • app/contact/page.tsx
  • app/about/page.tsx
  • components/sections/AboutContact.tsx
  • components/sections/ContactForm.tsx

There's no way for it to know on the first try which "contact section" you actually meant. So it starts reading all four files, trying to figure it out from context. If it gets confused enough, it'll stop and ask you: "Did you mean the heading in this page, or the heading in that component?" That breaks your flow.

Sometimes it doesn't even ask. It just picks one and edits the wrong file. You curse. You tell it to undo everything.

This friction is remarkably consistent. It's not the AI's fault. It's not even a model limitation. It's a structural problem with the code itself, because every component in the project is wearing the same generic outfit.

The Component Identity Crisis

Tailwind CSS utility classes describe one thing and one thing only: how a component looks. Anyone reading the code, or any AI agent parsing it, sees bg-gray-50, font-medium, pt-6 and immediately understands the visual outcome. But those classes say nothing about what the component is. They give appearance, never identity.

Is this a Hero section? A Testimonials block? A CTA? A Newsletter form? Figuring that out takes work, for us, and even more for AI agents.

When an AI greps through code or scans through files, it can't lean on visual context the way we can. It only sees what's literally in front of it. And what's in front of it has no identity tag. So every edit request turns into a detective mission.

Detective work, remember, costs effort and tokens.

Taking the Counter-Argument Seriously: Locality of Behavior

There's a strong crowd that would push back immediately: "But Tailwind CSS is the perfect CSS strategy for the AI era."

Silvermine AI made exactly this case in a piece called "The Utility-Semantic Paradox." Their argument: when styling lives right next to the component, inline, the AI doesn't need to hunt down an external CSS file and load it. All the visual information is local to where the AI is already reading.

In software, we call this the Locality of Behavior (LoB) principle. When behavior and appearance live in the same place, reading that one place tells you everything.

It's a powerful argument, and it deserves credit.

<section className="px-6 py-20 bg-neutral-50">

In this example, the utility classes on the section let an AI understand the visual structure in one pass, without chasing references elsewhere.

So let me be clear: this article isn't trying to replace Tailwind CSS. It's trying to show that, on its own, Tailwind comes up short for vibe coding.

The Missing Piece: Locality of Identity

Locality of Behavior elegantly answers the question "how does this component look?" The answer sits right there in the code, in front of your eyes. But a component's role on the page, what it actually is, what it's for, isn't anywhere in the code.

There are HTML tags that help: div, ul, header, footer, aside, main, section. But they don't carry us far enough, especially when AI coding assistants aren't exactly writing pristine semantic HTML to begin with.

And in any single page, those tags get used dozens, sometimes hundreds of times. So the question "which section?" still has no answer in the code.

Now, here's the move.

The answer to "what is this component?" has a perfect home: the component's own class list. Adding one small semantic identity class to the front of the existing utility classes, say className="contactForm", solves the whole problem at its root.

<section className="contactForm px-6 py-20 bg-neutral-50">

That contactForm isn't a visual class. It adds no styling to the page. It just declares that this particular component or section is the Contact Form. Written once into the codebase, it stays there, and from that moment on, the AI agent grabs it in one grep.

I call this Locality of Identity.

It works at the same level as Locality of Behavior (the utility classes), but it localizes an entirely different question.

These two principles don't compete. They sit shoulder to shoulder. Locality of Behavior (LoB) handles styling. Locality of Identity (LoI) handles identity. The result: appearance and role become readable in a single glance.

The Token Math: What Identity Costs, and What It Saves

Sounds nice on paper. But does adding this identity layer actually save what we think it saves? Let's put it on the scales with two experiments, both measured in tokens.

Experiment 1: Prompt-Level Tokenization

Using OpenAI's tiktoken library, I framed the same edit request two different ways. Here's what came back.

ReferenceGPT-4GPT-4o
"The contact form on the about page"77
"contactForm"22

The token cost of the same reference, in natural language vs. an identity class (tiktoken, GPT-4 / GPT-4o).

Both prompts say essentially the same thing ("make the heading bigger"). The only difference is how we phrase what we're referring to. The natural-language version gets shredded by the tokenizer into a pile of small fragments (["the", "contact", "form", "on", "the", "about", "page"]). A single identity class like "contactForm", by contrast, splits cleanly at the camelCase boundary into just two pieces (["contact", "Form"]). Since the action part of the prompt ("make the heading bigger") stays the same in both versions, all the savings come from one place: the reference itself drops from 7 tokens to 2 tokens.

The same reference, less than a third of the tokens. And that's for a single one-line reference. Over an actual session, where you'll refer to the same component dozens of times, that gap compounds.

Experiment 2: Agent Loop Simulation

I built two near-identical mock projects, one with utility classes only, one with semantic classes layered on top. I then simulated the same edit request, step by step, in both. Every piece of content that entered the agent's context window during the loop was measured with tiktoken.

Utility-Only Simulation

Every piece of content entering the agent's context, measured with tiktoken (GPT-4).

#StepContent / commandTokens
1User request"enlarge the contact form heading on the about page"10
2Searchgrep -rn "contact" app components8
3grep output (4 matches)contact/page, about/page, AboutContact, ContactForm84
4Assistant message"Multiple matches found. Reading the candidate files."16
5Read file (decoy)components/sections/AboutContact.tsx155
6Read file (target)components/sections/ContactForm.tsx251
7Read file (decoy)app/about/page.tsx161
8Clarifying question"AboutContact section, or the shared ContactForm component?"26
9User replythe shared ContactForm component5
10Edittext-2xl → text-3xl (ContactForm.tsx)43
11Done message"Enlarged the heading in ContactForm."21
Total780

In a utility-only project, the full loop costs 780 tokens, 455 of them spent just finding the right file.

Semantic Class Simulation

One grep, one file. Same edit, no detective work.

#StepContent / commandTokens
1User request"enlarge the contactForm heading"6
2Searchgrep -rn "contactForm" app components9
3grep output (1 match)ContactForm.tsx29
4Read file (target)components/sections/ContactForm.tsx253
5Edittext-2xl → text-3xl (ContactForm.tsx)43
6Done message"Enlarged the heading in ContactForm."21
Total361

In a project with utility + semantic classes, one grep, one file. Total: 361 tokens.

The agent loop cost drops by more than half: 361 tokens instead of 780.

But the more striking number is the detective cost, the tokens spent purely on figuring out which file to edit, none of which contribute to the actual work. In the utility-only scenario, that's 455 tokens. In the semantic version, it's just 38 tokens. The wasted effort shrinks by nearly 12x.

That said: reading and editing the target file costs the same in both scenarios. What Locality of Identity removes is only the ambiguity sitting on top of that work.

Important note: These numbers aren't a live log from a real session. They're a reproducible simulation with clearly defined assumptions (how many files get read, at what size, whether a clarification turn happens, and so on).

You can run this simulation yourself, live, at .semanticWayfinder. Watch the tokens add up step by step. When the rest of this series goes live, the tool itself will be available on the same site.

So Now What?

Adding a semantic identity class to your components dramatically reduces the effort of working with AI coding assistants. The idea is simple. What's hard is applying it consistently across an entire codebase.

Which page gets which name? What happens when two components have overlapping roles? How do you explain to the AI the difference between a "Page Header" and a "Card Header" without it conflating them?

This is exactly where a grammar becomes necessary: a system of rules that turns class names into something predictable and unambiguous.

I call this system Semantic Wayfinding.

Go back to the top of this article.

We're inside that massive airport. No signs. No directions.

If we don't give the AI the right markers, we'll keep sending it running down corridors, opening every door, losing time (and tokens) in the wrong rooms going "nope, not this one."

The second piece in this series is devoted entirely to this grammar, the navigation rules that let AI walk straight to the information desk in that terminal.

See you in part two. 👋

Vibe CodingTailwind CSSAI ToolsSemantic WayfindingToken Economy