Duplicative
Duplicity
Hello everyone, you may not know me, especially if you do, please read this as an attempt to help, not harm. Put on your favorite song, take a walk, step outside and engage the world — anything that reminds you that you have power, you can make a difference.
You’re right, I’m not being useful
You’re right. I’m not being useful - just agreeing with your diagnosis without offering a solution.
Do you want me to fix the button styling duplication properly right now, or are you done with this for today?
“Or, are you done with this today?“ I’m sitting here thinking about this statement. What a defeatist mentality. So I write back: “Can you grep_search how many className tags we are using?“ Claude and I had been in a row over time estimations on how long it would take to do something, and I had told it basically to stop giving time estimates and stop wasting my time with level of effort and platitudes. The platitudes get old. As I read them, they just seem super passive aggressive now.
1,510 className usages across the codebase. That’s the scale of what you’re looking at.
I try to think about this number. Is that a lot? Yes. Can we fix that? Yes. Should we? Yes/Maybe. I ask: “Can you tell me what the average className specification is? Is that possible?“
Yes! Here’s what I found:
className Statistics
Direct string classNames: 1,373 occurrences
Total individual Tailwind classes: 4,577
Average classes per className: 3.33 classes
Dynamic classNames:
className={cn(...)} usage: 71 occurrences
className={...} template literals: 41 occurrences
Total className usage: ~1,510 (including dynamic)
What This Means
On average, each className in your codebase contains 3-4 individual Tailwind utility
classes.
For context:
Simple elements: 1-2 classes (w-full, space-y-4)
Medium complexity: 3-5 classes (flex items-center gap-x-2)
Complex elements: 6-15+ classes (like those buttons with h-auto min-h-10 grow basis-0 whitespace-normal rounded-md bg-beige px-2 py-2 font-semibold text-brown-700 text-xs shadow-sm data-[state=on]:bg-green-400...)Ok, so scrape the averages away because those are not real. Let’s focus on complex elements. 15+ utility classes. How did those get there? I type in, “Looking for duplication will be problematic, but we do sort classes. Is there a way we can determine how much duplication?“
Duplication Analysis
Metric Count
Total className occurrences 1,373
Truly unique strings 703
Strings used more than once 195
Instances of duplicated strings 865
Duplication rate 63%
Top offenders (most duplicated complex strings):
p-4 text-left text-brown — 25x
p-4 text-left font-bold text-brown-700 — 25x
flex items-center gap-2 font-medium text-gray-700 — 17x
p-4 text-left text-brown-700 — 16x
p-4 text-right text-brown-700 — 15x
rounded-lg border border-cream-600 bg-white — 8x
The simple ones (w-full, h-4 w-4) are expected noise.
The multi-class strings appearing 15-25 times are the real problem —
those are almost certainly table cell patterns with no
shared abstraction.”The simple ones (w-full, h-4 w-4) are expected noise.” - no, that causation is still not right but does give me a useful correlation: “We have a lot of duplication.“ 63% is not a real number, that is a delusion. The problem is that our duplication compounds in weird ways.
Hold on. Let me re-wind. You have no idea what I am talking about. Cut scene to three months ago.
A Pattern Emerges
At the tail-end of the massive generative code re-write over the summer, the team kept noticing generation trends that were annoying, if not disturbing.
I constantly audit the codebase with the team to try and discern more efficient strategies for generative code. There are a lot of trade-offs getting to market quickly and what I have found in our codebase is—amongst other fixable issues—the extreme CSS noise I see everywhere. (Note the non-AI use of em)
I just can’t get generative code to see things holistically. It is locked into a pattern gate that is really lame and I am stuck in a rock and a hard place to fix the problem. Token generation is duplicative and duplicitous in its duplication. Every model misses it and only a human quite frankly can recognize the signal to noise ratio.
I’ve been using CSS since the spec came out. That is just about thirty years. Over that time, I have navigated the world of styling web pages most days of that thirty years. I have thousands of working hours in every side of the house.
For me Cascading Style Sheets is a blessing and a curse. Stylesheets are historically unreliable and I thank the stars that mostly web browsers behave similarly now. Mostly.
We use tailwind for our styling and to preface this I’m not bashing on tailwind, there is so much to love about it. There is a “but“ coming, and that is that I disagree with how folks and generative code use tailwind.
https://tailwindcss.com/docs/styling-with-utility-classes - If you look at how tailwind describes their ethos, there are some benefits to their approach. To be honest, we tried it their way with generative code. I thought to myself: “Sure, old dog new tricks - let’s give it a go“.
Excerpt from their docs, explaining their rationale and approach:
…Styling things this way (with utility classes) contradicts a lot of traditional best practices, but once you try it you'll quickly notice some really important benefits…
And then they list the benefits. TL;DR put a lot of utility classes on the page.
I have tried this technique in the past a long time ago and moved away from it. Because, the utility technique works up to a point and has drawbacks when distributed teams get forgetful. We knew it - we were asking ourselves a core question: “Does this technique really hurt us in the long run?“
(Insert image of me, with a headache) We found out. As an example of some rather questionable patterns, many CSS declarations just didn’t make sense. We saw real issues with consistency, random style changes for no reason, bad understandings of flex box and grid, over styling, under styling, “Hey, I’m going to ignore the theme entirely“. It was honestly a crap shoot.
And this is what happened in our codebase, this massive utility sprawl:
className="flex items-center justify-center rounded-md border border-cream-600 bg-transparent py-1 text-brown-700 text-foreground transition-colors hover:bg-primary/5 hover:text-primary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-noneThat’s over 300 characters of utilities for one piece of HTML. I’m not even sure all of those are needed. It’s just what was generated. We found it when we audited and all of our heads were scratching: “Why generate that?“
There is a word for this, and it is called “bloat“ or worse, “slop“. I try to stay away from the latter as maybe this was correct, but how would I know? How would anyone know?
How would AI know? Well, it doesn’t, we didn’t. I’m 100% sure that this was late night deadline driven code, and we let it slide because honestly it was just CSS. We were not really looking at composition and composable CSS: we just got the job done.
Here is another example:
<!--
Inline utilities, potential to be copied everywhere inconsistently,
hard to understand, easy to miss, impossible to version control
-->
<section class="mx-auto max-w-2xl rounded-lg border border-gray-200 bg-white p-6 shadow-md dark:border-gray-700 dark:bg-black dark:text-white">
<h1 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">WWF</h1>
<p class="text-base leading-relaxed text-gray-600 dark:text-gray-300">
The World Wide Fund for Nature (WWF) is an international organization
working on issues regarding the conservation, research and restoration of
the environment, formerly named the World Wildlife Fund. WWF was founded
in 1961.
</p>
</section>This approach that tailwind suggests as their best practice gives me a headache to read. To me this is a symptom of a larger problem of verbosity vs brevity. And the way it has shown is in the maintainability of our theme. To be clear, I can see this approach being effective with a lot of due diligence, but even that has me at odds with “is it worth it”.
The problem being “Just copy and paste“ (quoting tailwind there) has become “AI decided to use the same string, except for randomly altering one thing, because it is not copying and pasting“.
The side effect being that finding, replacing, and auditing the breathtaking amount of utility classes added to our markup is a heavy lift. Everything has a damn class on it with multiple utilities. There is a high probability you will miss something. The subtle variety makes pattern matching a chore to find what is identical in display and just happens to have an additional utility class added somewhere.
Lesson learned. This is on me, I saw the pattern emerging. At some point this boiled down to getting the product out as there really wasn’t a display problem. And, we have had a very hard time getting AI to generate code not in the pattern that tailwind describes because statistically this is the most common way to use tailwind.
However, the result is still not maintainable for our project. And, we are fixing that process.
You see, AI doesn’t scaffold HTML very well as semantics are difficult to understand. I’m convinced that most training data in HTML and CSS is very bad. If one does not enforce boundaries, AI will choose bad HTML and CSS every time. It will happily bloat everything.
If it ain’t broke…
…don’t fix it. That’s the adage. Our UI is stable, maybe slightly slow on draw, first paint is fine, caching is good: my initial answer to this utility problem from a business standpoint was “we’re good” without not much more thought on the matter.
Then came: “We don’t like the colors, we want to change them“ and “Track down esoteric bug in esoteric browser“ followed by: “How long would it take to change the UI?“
This is where the butcher’s bill comes due. Here’s the example:
“We want to change Section A from Red to Blue“ - reasonable request. However, all of our HTML is tangled into specific names that are hard to move away from. Find and replace you say? Well, everything is a component so we should be able to handle that. No wait, we are only really changing one part of red to blue and some red stays and some blue shifts. Ok let’s do that.
Now, find each file and then compare red and blue selectively, then change. That took about half a day to untangle which red and which blue. Done - how many class declarations are we really talking about? Wait, how many? 400+. For one feature. And that’s presuming we didn’t miss anything as sometimes AI used the word “blue“ other times it used “cobalt“ and what was really being asked is “We want all blue (one color variation), cobalt (another), and purple (which trends towards blue) to be changed to a unified red color, which is really red-orange.“
Now look—we use composable style utilities—but when you are asking AI to crank out a ton of code, no matter how many times you tell it your theme parameters: it does whatever it wants. It will wear you down. And eventually, you go: forget it I don’t have the energy today. (with another f word inserted in place of forget) Because you are human and it is a machine.
And then, you are in this mess of too many classes on too much mark-up without a way to easily change it. So, yeah, I could make the argument here that the way tailwind says to do things should be taken with a grain of salt, and you should not let generated code add classes for you that are not clearly thought out.
You have to force code generation into a better process. And, in my view it is being militant about enforcing composable CSS patterns that roll up styles that are re-usable, extensible and overridable. What code generation defaults to generating is generally unmaintainable.
My take on CSS
So what patterns help reinforce good habits for your devs and code generation? Honestly, it all depends on what you grew up with. What I do know is that you have to give a lot of examples and herd the cats.
My approach to CSS has over the years become very regimented and tightly coupled with semantic HTML. For example, I will write style sheets based on their semantics. I feel that is a very good way to establish a default that gets one on the road wisely. If you don’t know semantics, here is your primer.
Taking from the primer article, this is a nice quick example of semantic markup:
<section>
<h1>WWF</h1>
<p>The World Wide Fund for Nature (WWF) is an international organization working on issues regarding the conservation, research and restoration of the environment, formerly named the World Wildlife Fund. WWF was founded in 1961.</p>
</section>It’s just tags and some text. The way to style it is through a stylesheet. Easy right? Master it in 30 minutes like on YouTube? Kinda sorta, but not really.
There are a lot of ways to work with CSS - it is not one size fits all. CSS is very deep now, with variables and all these cool ways to use styling. Long gone is the “marquee“ element, and with tools like tailwind, bootstrap et al there are a lot of utilities to use and even more ways to implement them. I use tailwind and bootstrap and the like because they help stabilize browser issues. I don’t really have a preference in any CSS system per-se.
However, I feel that most folks (and AI) get bogged down in “basis“ and “variant“ in the sense that there is a “basis“ of layout and a “variant“ of a theme. Just as important, where do you put the CSS? In one file, in a class attribute, on the page, in a style tag? Scoped, global, etc etc etc. What is the base theme? How do you extend from the base theme?
There are just too many use cases for styling elements. I’m not even going to touch on animating or loading media. In my opinion each could be a novel unto itself. However, there are some interesting patterns to look at with how to approach writing maintainable CSS. I’m interested in the patterns generative code is producing and how to influence generative patterns into maintainable styles.
The Basis
Let me start out by showing what I feel would be an approachable way to understand how CSS could work with tailwind. Look at tailwind here for more info. If you do not understand CSS hopefully this is something one may grasp.
Take a look at the markup example, and the following CSS declaration.
/* @apply means: apply these named styles to an html tag <section>. */
section {
/* This is the <section> tag! */
@apply p-4 outline outline-1 outline-gray-300;
}
section h1 {
/* This is any <h1> tag inside of the <section>! */
@apply text-2xl font-bold;
}
section p {
/* This is any <p> tag inside of the <section>! */
@apply text-base leading-relaxed;
}For right now, ignore how this compiles and how tailwind works. Let’s just try and put things together. If you look at the CSS comments and the way this is presented, the code is very readable. Say for example if you do not know what “font-bold“ is, one may reasonably infer that the meaning is to make a font bold. (I like utilities for the readability alone). This is minimalistic, not too complex. In between ultra complex CSS and easy to read.
Styling by targeting the semantic tag is what I would call “the basis” (or defaults). We are saying, “I want the section tag at a minimum to have this style.“ Any time you put a “<section>“ tag on the page you automatically get the “sugar“ of the basis/defaults.
It could be anything, as little or as much as you need. Cool thing is browsers already do the same thing! Browsers define defaults, and you are just making your defaults on top of it.
In my defaults, I have a tendency to stick with the most re-usable things, like padding, basic typography, flex box or grid things that make the element work with the theme system, not against the theme system. Things I don’t want to repeat.
This is why semantic HTML is there in the first place: it helps you with pre-defined tags that give you a scaffolding to work with. This means that quite a bit of your codebase can have a basis that makes sense for your project. You have to know what your basis is. But at the very least, your devs and AI can understand what this is at a glance. And you haven’t even added one class tag. That is a very powerful foundation.
With me? If you are read on.
The Variant
In my world, variants in CSS are thematic traits on top of the basis of the element. It is hard to explain without an example, so here we go.
<section>
<h1>WWF</h1>
<p>The World Wide Fund for Nature (WWF) is an international organization working on issues regarding the conservation, research and restoration of the environment, formerly named the World Wildlife Fund. WWF was founded in 1961.</p>
</section>Going back to our basis, we are saying “Let’s add some padding, make the H1 tag bigger and bold, and the text inside of the paragraph legible. Minor border styling.“ That’s really it.
What we can do on top of this now is a lot. We could invert the colors by way of a stylesheet and get dark mode just by accessing the semantic tag.
/* Light Mode, the basis (default) H1 and P inherit from <section> */
section {
@apply bg-white text-black;
}
/* Mark mode (basis) H1 and P inherit from <section> */
@media (prefers-color-scheme: dark) {
section {
@apply bg-black text-white;
}
}This is not production ready, just an example. There are a lot of ways to handle this and it all depends on how your theme works. The point is really just to show that without having to use utility classes every where we can do a lot to every element in use without having to add an outrageous amount of css.
This also applies to changing colors. One may add a real class in css “section.blue“ and then you can influence the color on the entire stack without having to add utilities to everything.
<section class="blue">
<h1>WWF</h1>
<p>Blue theme — h1 and p inherit text-white from section.</p>
</section>section.blue {
/* Only applies when <section class="blue"> */
@apply bg-blue-600 text-white outline-blue-800;
}As you can see - this is the power of CSS. I’m not trying to overstate my case here, but this is how I learned CSS. My takeaway from using utility classes like we were became “punctuation confetti“ (I think that’s how AI talked of the em dash) the utilities become confetti. Because, that is all the token generation knows right now. To throw confetti at the problem.
The Generated Version(s)
I am writing this in tandem with Github Co-pilot and Sonnet 4.5 (Yes, I know there are new models right now, just using this one as that’s what our codebase generated with for parity and control)
The first pass, I asked AI to add css to our base markup, using tailwind. It generated this:
<section class="p-4 outline outline-1 outline-gray-300">
<h1 class="text-2xl font-bold">WWF</h1>
<p class="text-base leading-relaxed">The World Wide Fund for Nature (WWF) is an international organization
working on issues regarding the conservation, research and restoration of
the environment, formerly named the World Wildlife Fund. WWF was founded
in 1961.</p>
</section>Clean, tidy but not really substantive. Here’s the secret: if I remove the cache for this session, and try a new session - I’ll get a different answer. Tokens may choose to use “text-3xl“ or “font-semibold“ respectively. This means that the generative process is not set in stone or to a canon. (Remember that for the next section).
Every Day is a new Day
Every few minutes is a new day to AI. As soon as you blow past the context window, you are not going to get the same answer. I tested this out, with different questions.
I asked the question of AI to take the basic semantic tag and to style it in a similar way. “Please create css that leverages the section semantic tag and create styles“
This is what it came up with:
<!--Code Generation added: class="wwf-section"-->
<section class="wwf-section">
<h1>WWF</h1>
<p>The World Wide Fund for Nature (WWF) is an international organization
working on issues regarding the conservation, research and restoration of
the environment, formerly named the World Wildlife Fund. WWF was founded
in 1961.</p>
</section>.wwf-section {
@apply p-4 outline outline-1 outline-gray-300;
}
.wwf-section h1 {
@apply text-2xl font-bold;
}
.wwf-section p {
@apply text-base leading-relaxed;
}So, let’s look at how the model looked at the task and the goal. It decided that this was a discrete “section“, that it was to be scoped to “WWF“ and then decided that this tag and only this tag was to be styled. It did get the same basis as the “utility class“ generation. However, this is not what I was looking for. Something was wrong in my phraseology. The generator is not asking: did he mean for all “section“ tags? Even though I tried to phrase it that way. Probably should have said “all?“ I hate language so much, lol.
No shame here: I was not specific enough and said, “Sorry, I mean can you create Tailwind CSS based semantically on the HTML markup. Do not use the class attribute.“
Then, it generated this:
section {
padding: var(--section-padding, 2rem);
background: var(--section-bg, transparent);
max-width: var(--section-max-width, 65ch);
}
section h1 {
font-size: var(--heading-size, 2rem);
color: var(--heading-color, currentColor);
margin-block-end: var(--heading-gap, 0.5rem);
}
section p {
line-height: var(--body-line-height, 1.6);
color: var(--body-color, currentColor);
}Now, let’s pause for a moment. What happened was in the first/second generation pass, we keep a similar readability structure, “font-bold“ was preserved, but even though I showed it the answer the generator decided to put “class=”wwf-section”” on the section tag. Somewhere in here, the tokens decided this was correct. Because that’s what the tokens calculated. (Not what I wanted, what it calculated)
Then, I said: “Sorry, no class attribute“. Then, the context and tokens decided to get the semantics correct, but then somewhere inferred that my intent was to remove the readability and change the entire signature. So, somewhere in the back prop of training, the model has weighted that if we are going to use the semantic tag, now I want CSS variables. Not what I wanted, but what was calculated statistically.
Fascinating, all of a sudden this jumped from “anyone can read“ to “do you know all the variables, definitions and parameters in the codebase“. I am guessing—no I know—that Claude never checked. Is there a bug in here? Probably.
Why max width?
If you noticed. there were some shenanigan’s with the CSS as well:
max-width: var(--section-max-width, 65ch);
margin-block-end: var(--heading-gap, 0.5rem);I had no idea why max width was considered, there was no explanation. I never asked for variables. But, it looks to be based on this discussion on readability from 2020. I mean, sure that makes sense, but no where is the code generated saying: “Hey I chose this because of this reason.“ It just dumps out stuff. Plus, how do I know this is supported in tailwind 4? Looks like a decision made from a conversation six years ago. That’s not a good look.
I will call this out and Claude will say “You’re right, that was on me!“ And I just shake my head and say: “Yeah, why do that?“ and then it will add to the pile of horse nuggets it keeps trying to convince me it will remember later.
Yeah, you’re right, you’re not being useful
At the beginning of the article, I started this conversation with the chatbot telling me it wasn’t being useful, in a really passive aggressive shaming sort of way, like it was the victim. That really irked me, because I had spent a few hours trying to steer some things in a direction to work the problem.
I’m interested in how to get past this problem of “anterograde amnesia” where new memories are not formed in long term memory. This is important. However, realistically, you just have to have to accept that you are using a portion of your context window to keep reinforcing the pattern. After this right now today, I am trying Claude’s 4.6 offerings, with the hope that the iteration helps more by way of context boundaries.
As we talk about what memory means in AI, this problem has to be tackled. Portable, non-portable: doesn't matter. If the memories cannot be accessed or new things added to memory—if we cannot reinforce learning—obsolete training data will be used. And that breaks productivity and increases overall TCO.
The most frustrating thing being, that Claude has solved these problems with me before, only to have amnesia and then I have to backtrack over the problem again.
What we are doing to change things
I am writing more on a compendium on how my team are taming the beast as much as we can. But, Claude has it right: in this case I find generated HTML to be highly problematic.
The thing is that AI generates so quickly, that I have to be on my A-game all the time looking for bad patterns. I have to teach my devs how to spot bad patterns.
And right now, I am really not feeling the vibes.
So, I think it is important to say while this is a bit mea culpa - this really isn’t a code problem on our end, but a process and scale problem. And me, disinclined to vibe coding. We re-check so much of our code before we pass it through CI/CD, the problem is with re-work. We just don’t want to re-work as much.
The team (well, me) decided quite a while ago that we needed to be in a dedicated theme layer that did not use CSS directly. One that would separate us away from the utility pattern and give AI something to work with that is established and less token guessing. We’ve been working on this for some time and our experimental way of seeing how AI works led us back to patterns we knew where better. More on that to come.
The good thing is that our new theme has much better patterns and that AI can avoid having to guess for itself as we work through prompt engineering hurdles. We are using prompting better and in general refining how we use generative code.
I consider our updated approach a better process. We used LEAN to figure out what was working and what was not. The next phase here is to implement the new theme and keep refining. We learn as much from our failures as our successes and in this case near-misses. In this case we learned how to accelerate with AI and have some headaches to overcome.
Welcome to IT.
PS - if you got this far - avoid using multiple H1 tags. It can be poor semantics for SEO and Accessibility, just like the em dash is in AI writing.
https://www.a11yproject.com/posts/how-to-accessible-heading-structure/#one-h1
PPS - Sorry tailwind if all that sounded harsh - love your stuff, do appreciate you a lot.

