How AI Coding Agents Actually Generate CSS (And Why It's Always the Same)
After `bg-`, a model is far likelier to write blue than fuchsia, not because blue looks better but because that is what it learned. Here is the token-probability mechanism that makes AI sites alike, and why temperature can't fix it.
On this page
Ask Cursor, v0, Lovable, and Claude Code to build the same landing page, and the four files will share a fingerprint: bg-blue-600 buttons, rounded-xl shadow-lg cards, Inter, grid-cols-3, py-24. Different models, different companies, same CSS. That is not a coincidence or a failure of imagination. It is a predictable consequence of how large language models tokenize, sample, and generate CSS tokens. Understand that mechanism and you understand why every fix that targets the prompt fails — and why Sailop targets the context instead.
This article goes deep into the machine. If you want the high-level overview of why AI sites look the same, read why every AI-generated website looks the same first. If you want the patterns themselves, our catalog of 90+ AI design patterns has them. This one is for people who want to understand the gears.
How LLMs Tokenize CSS
Large language models do not see CSS the way you do. You see bg-blue-500 and think "a medium blue background." The model sees a sequence of tokens. Depending on the tokenizer, bg-blue-500 might split into two tokens (bg-blue and -500), three (bg, -blue, -500), or stay a single token if it appears often enough in training data.
Here is the kind of split a byte-pair tokenizer produces on common Tailwind classes. The boundaries below are illustrative; the exact ones depend on the tokenizer:
Token breakdown (illustrative):
"bg-blue-500" -> ["bg", "-blue", "-", "500"] 4 tokens
"bg-amber-400" -> ["bg", "-am", "ber", "-", "400"] 5 tokens
"bg-fuchsia-300" -> ["bg", "-f", "uch", "sia", "-300"] 5 tokens
"text-slate-700" -> ["text", "-sl", "ate", "-", "700"] 5 tokens
"rounded-lg" -> ["rounded", "-lg"] 2 tokens
"shadow-md" -> ["shadow", "-md"] 2 tokens
"p-6" -> ["p", "-", "6"] 3 tokensThe pattern to notice: a common word like blue tends to survive as one clean token while a rarer one like fuchsia shatters into -f, uch, sia. Common color names earned their own slots in the vocabulary; rare ones get reassembled from fragments. That fragmentation is the first tax on diversity — but token count is the small problem. The real issue is token probability.
The Token Probability Problem
When an LLM generates text, it predicts the next token from everything before it. At each step it produces a probability distribution over its entire vocabulary, then samples one token (with randomness tuned by temperature).
Here is the critical insight: common CSS patterns have dramatically higher token probabilities than uncommon ones.
Consider what happens after the model has written class="bg- and needs the next token. The table below shows the shape of that distribution. The numbers are illustrative: no vendor publishes them, and they differ between models and versions.
Next token prediction after "bg-" (illustrative):
+------------------+-------------+
| Token | Probability |
+------------------+-------------+
| blue | 0.23 |
| white | 0.18 |
| gray | 0.15 |
| black | 0.09 |
| green | 0.07 |
| red | 0.06 |
| slate | 0.04 |
| indigo | 0.03 |
| purple | 0.02 |
| yellow | 0.02 |
| amber | 0.01 |
| emerald | 0.01 |
| fuchsia | 0.004 |
| rose | 0.003 |
| cyan | 0.003 |
| lime | 0.002 |
| teal | 0.002 |
| (other) | 0.087 |
+------------------+-------------+In a distribution shaped like this, blue is not slightly ahead. It is an order of magnitude ahead of fuchsia. Not because blue is a better color — because blue classes appear far more often in the code the model learned from: tutorials, starters, component libraries, answers about Tailwind. That is how bg-blue-500 became the new Comic Sans.
The same skew holds for the shade after the color:
Next token prediction after "bg-blue-" (illustrative):
+-------+-------------+
| Token | Probability |
+-------+-------------+
| 500 | 0.34 |
| 600 | 0.19 |
| 700 | 0.14 |
| 400 | 0.09 |
| 100 | 0.07 |
| 200 | 0.05 |
| 300 | 0.04 |
| 800 | 0.03 |
| 900 | 0.02 |
| 50 | 0.02 |
| (eos) | 0.01 |
+-------+-------------+The 500 shade wins, not because it is right for every context but because it is the default example shade in the docs. Chain the two distributions and bg-blue-500 is the single most probable background-color sequence in virtually every LLM trained on web code. The model is not choosing it. It is falling toward it.
The Training Data Bias
Nobody outside the labs can count what is in a model's training data, and an earlier version of this article printed frequency charts for colors, fonts and grids that we could not source. What can be checked is the public material these models learn from, and what their makers say about the result:
- Tailwind's own docs lean on a few families. On the page that teaches utility classes ("Styling with utility classes", counted on 17 October 2026), gray and slate classes dominate, followed by sky, the blue of Tailwind's own brand. Docs, tutorials and starters like these are copied into countless repositories.
- Model makers recommend the same stack. OpenAI's GPT-5 prompting guide suggests Tailwind CSS, shadcn/ui and fonts such as Inter and Geist for new frontends.
- Model makers name the result. Anthropic's frontend aesthetics cookbook (October 2025) says that without guidance Claude "tends toward generic, conservative designs", and lists "Overused font families (Inter, Roboto, Arial, system fonts)" and "purple gradients on white backgrounds" among the clichés to avoid.
- The web itself is less uniform than the output. The HTTP Archive's Web Almanac 2025 finds Inter on roughly 1.4 to 1.5% of pages, far behind Roboto (around 10%). The default is strong in generated code, not across the web, which is exactly why it stands out — and why Inter is killing your brand.
Three-column grids follow the same path: they are the shape of the feature section in most starters, which is why every agent reaches for grid-cols-3 on a feature section — a pattern pervasive enough that we gave the three-card grid its own autopsy.
Why Temperature and Top-p Do Not Fix This
The obvious objection: "Just raise the temperature." Temperature controls how much randomness the model injects during sampling. Higher temperature flattens the distribution, lifting less common tokens.
Here is what temperature does to an illustrative bg- distribution like the one above:
Token probabilities at different temperatures (illustrative):
Temperature 0.0 (greedy):
blue ████████████████████████████████ 1.00
(everything else is 0)
Temperature 0.7 (default):
blue ████████████████████████ 0.31
white ██████████████ 0.18
gray ████████████ 0.15
green ████████ 0.10
red ██████ 0.08
...
Temperature 1.0 (neutral):
blue ████████████████████ 0.23
white ██████████████ 0.18
gray ████████████ 0.15
green ████████ 0.07
red ██████ 0.06
...
Temperature 1.5 (high):
blue ████████████ 0.14
white ██████████ 0.12
gray █████████ 0.11
green ████████ 0.09
red ███████ 0.08
indigo ██████ 0.07
purple █████ 0.06
yellow █████ 0.05
amber ████ 0.04
...Even at a high temperature, the token that led keeps the lead. Flattening the distribution enough to change the ranking means pushing temperature so high that the model starts generating garbage. Class names go malformed (bg-bl-ue-50x), property values turn invalid, and the model "forgets" how to write correct CSS because randomness has drowned the learned grammar. You traded one bad outcome (generic) for a worse one (broken).
Top-p (nucleus sampling) has the same limitation. It restricts sampling to the smallest set of tokens whose cumulative probability clears the threshold. At top-p 0.9 you are still drawing from the same high-probability head:
Sampling visualization (illustrative):
Top-p = 0.9 includes these tokens:
[blue, white, gray, green, red, indigo, purple, yellow]
^
cumulative probability = 0.91
Top-p = 0.99 includes these tokens:
[blue, white, gray, green, red, indigo, purple, yellow, slate, amber, emerald]
^
cumulative probability = 0.92
Still no fuchsia, rose, cyan, or lime.The fundamental problem is that randomness is the wrong tool for design diversity. You do not want random CSS. You want intentionally different CSS that still obeys design principles. That is the job of constraints.
How Constraints Work Better Than Randomness
Sailop takes the opposite approach to temperature. Instead of adding randomness to the generation process, it adds constraints that redirect the model toward specific, coherent design choices.
Here is how a skill like Sailop's (designed, not published yet; see install) is meant to work inside an agent like Claude Code:
Standard LLM generation:
User: "Build a landing page for my SaaS product"
|
v
+------------------+
| LLM generates |
| tokens using |
| default probs |
+------------------+
|
v
bg-blue-500, Inter, grid-cols-3,
rounded-lg, shadow-md, p-6
(same as every other site)
Sailop-constrained generation:
User: "Build a landing page for my SaaS product"
|
v
+------------------+
| Sailop skill |
| pre-loads rules |
| into context |
+------------------+
|
v
+------------------+
| LLM generates |
| tokens with |
| constraints |
+------------------+
|
v
bg-amber-800, Space Grotesk,
asymmetric grid, custom borders,
no shadows (unique to this site)The mechanism is context injection. When the Sailop skill activates, it injects design rules into the context window. Those rules act as strong priors that override the training-data distribution. The model does not need randomness to avoid blue-500, because the rules explicitly say to.
How Rule Injection Changes Token Probabilities
When the context contains an instruction like "Use amber as the primary color, never use blue," the distribution shifts hard:
Next token prediction after "bg-" WITH rules in context (illustrative):
+------------------+-------------+
| Token | Probability |
+------------------+-------------+
| amber | 0.41 |
| stone | 0.12 |
| neutral | 0.08 |
| warm | 0.07 |
| zinc | 0.06 |
| blue | 0.002 |
| (other) | 0.258 |
+------------------+-------------+In this illustration the instruction inverts the distribution: amber moves to the head, blue nearly disappears. This beats temperature because it is targeted. The model still emits valid CSS with high confidence. It just emits different valid CSS.
The Sailop Skill Architecture
How the skill pre-loads rules into an agent:
1. User installs Sailop skill:
sailop install-skill
2. Skill registers with the AI agent's tool system
(MCP protocol or native skill format)
3. When a design task is detected, the skill activates:
+------------------------------------------+
| Sailop Skill Activation |
| |
| 1. Load generated design system |
| (colors, fonts, spacing, etc.) |
| |
| 2. Load anti-pattern rules |
| (73 rules from the manifesto) |
| |
| 3. Inject as system-level context |
| (highest priority in attention) |
| |
| 4. Monitor generated output |
| (flag violations in real time) |
+------------------------------------------+
4. Every subsequent LLM generation is influenced
by the injected constraintsFor how this rides on MCP, see MCP servers for design. For the full ruleset, read the anti-slop manifesto.
Code Examples: Default vs Constrained Output
Here is the shape of the difference on a pricing section. Both versions are hand-written illustrations of typical output, not captured runs.
Without Sailop (Default Generation)
Prompt: "Create a pricing section with three tiers"
// Claude's default output (no constraints)
export function Pricing() {
return (
<section className="py-24 bg-white">
<div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-bold text-center text-gray-900 mb-12">
Simple, transparent pricing
</h2>
<div className="grid grid-cols-3 gap-8">
{plans.map((plan) => (
<div
key={plan.name}
className="bg-white rounded-xl shadow-lg p-8 border border-gray-200"
>
<h3 className="text-xl font-semibold text-gray-900">
{plan.name}
</h3>
<p className="text-4xl font-bold text-gray-900 mt-4">
{plan.price}
</p>
<ul className="mt-6 space-y-3">
{plan.features.map((f) => (
<li key={f} className="flex items-center text-gray-600">
<CheckIcon className="h-5 w-5 text-blue-500 mr-2" />
{f}
</li>
))}
</ul>
<button className="mt-8 w-full bg-blue-600 text-white rounded-lg py-3 font-semibold hover:bg-blue-700">
Get started
</button>
</div>
))}
</div>
</div>
</section>
);
}Count the tells: grid-cols-3, rounded-xl, shadow-lg, bg-blue-600, text-gray-900, three identical cards, centered "Simple, transparent pricing" headline — even the copy is a documented slop phrase. Seven patterns from the 23 signs of AI-generated code. It would score around 72 on the Sailop scale (Grade D).
With Sailop Constraints
Same prompt, with the skill active and a generated design system loaded:
// Constrained version (illustration)
export function Pricing() {
return (
<section className="py-20 bg-stone-50">
<div className="max-w-5xl mx-auto px-6">
<div className="mb-16">
<p className="text-sm font-mono tracking-widest text-amber-700 uppercase">
Pricing
</p>
<h2 className="text-4xl font-light text-stone-900 mt-2 max-w-lg">
Pay for what you ship, not what you store
</h2>
</div>
<div className="grid grid-cols-[1fr_1.5fr_1fr] gap-0 border border-stone-300">
<div className="p-8 border-r border-stone-300">
<h3 className="font-mono text-sm tracking-wider text-stone-500 uppercase">
{plans[0].name}
</h3>
<p className="text-3xl font-light text-stone-900 mt-3">
{plans[0].price}
</p>
<ul className="mt-8 space-y-2 text-stone-600 text-sm">
{plans[0].features.map((f) => (
<li key={f} className="py-1 border-b border-stone-200">
{f}
</li>
))}
</ul>
<a
href="#"
className="inline-block mt-8 text-sm font-mono text-amber-800 underline underline-offset-4"
>
Start free
</a>
</div>
<div className="p-8 bg-stone-900 text-stone-100 border-r border-stone-700">
<h3 className="font-mono text-sm tracking-wider text-amber-400 uppercase">
{plans[1].name}
</h3>
<p className="text-3xl font-light mt-3">{plans[1].price}</p>
<ul className="mt-8 space-y-2 text-stone-300 text-sm">
{plans[1].features.map((f) => (
<li key={f} className="py-1 border-b border-stone-700">
{f}
</li>
))}
</ul>
<a
href="#"
className="inline-block mt-8 text-sm font-mono text-amber-400 border border-amber-400 px-4 py-2"
>
Start trial
</a>
</div>
<div className="p-8">
<h3 className="font-mono text-sm tracking-wider text-stone-500 uppercase">
{plans[2].name}
</h3>
<p className="text-3xl font-light text-stone-900 mt-3">
{plans[2].price}
</p>
<ul className="mt-8 space-y-2 text-stone-600 text-sm">
{plans[2].features.map((f) => (
<li key={f} className="py-1 border-b border-stone-200">
{f}
</li>
))}
</ul>
<a
href="#"
className="inline-block mt-8 text-sm font-mono text-stone-600 underline underline-offset-4"
>
Contact us
</a>
</div>
</div>
</div>
</section>
);
}This version uses an asymmetric grid (grid-cols-[1fr_1.5fr_1fr]), zero rounded corners, zero shadows, a stone-and-amber palette instead of blue, monospace labels, hairline borders instead of floating cards, and a darkened center column that carries the featured tier without a "MOST POPULAR" badge. It would score around 23 on the Sailop scale (Grade A).
The difference is not luck. The skill injected concrete constraints: stone-and-amber palette, borders over shadows, monospace for UI labels, broken grid symmetry for the featured tier. Each constraint narrows the token distribution toward one direction. For more layouts that escape the three-identical-cards trap, see 15 anti-slop pricing page layouts.
The Mathematics of Constraint vs Randomness
To formalize why constraints beat temperature, picture the space of all possible CSS outputs for one component.
Design space visualization:
All possible CSS outputs
+----------------------------------+
| |
| +-------+ |
| | Good | +-------+ |
| |unique | | Good | |
| |design | |unique | |
| +-------+ |design | |
| * T=0 +-------+ |
| (default) |
| |
| T=1.5 samples from here: |
| * * * * * * |
| * * * * * |
| (mostly bad, some OK) |
| |
| Sailop constrains to here: |
| [====] |
| (small region, all good) |
+----------------------------------+
T=0: One point. Always the same. Always generic.
T=1.5: Scattered points. Mostly broken CSS.
Sailop: Narrow region. Valid and unique.Temperature widens exploration into regions full of invalid CSS. Constraints confine exploration to a region that is both valid and unique. This is why constraints beat templates for design generation.
The Attention Mechanism and Rule Priority
One more technical detail. When Sailop injects rules into the context window, those rules compete with the model's training-data priors for influence. The attention mechanism decides who wins.
Rules placed in system prompts or skill preambles carry high attention weight for three reasons:
- Position bias. Tokens at the start and end of the context window get more attention than tokens buried in the middle. Sailop rules sit at the system level — the start, the highest-priority position.
- Instruction following. Modern LLMs are fine-tuned to obey explicit instructions. "Never use rounded-lg" reads as a directive, not a suggestion.
- Specificity. "Make it unique" has weak influence — the model cannot resolve it into token choices. "Use border-2 border-stone-300 instead of rounded-lg shadow-md" resolves directly into a probability shift.
Attention weight distribution:
+----------------------------------------------+
| System prompt (Sailop rules) [████████] | High attention
| User message [██████] | Medium-high
| Conversation history [████] | Medium
| Training data prior [██] | Low (overridden)
+----------------------------------------------+
When rules conflict with training data priors,
the rules win because they have higher attention weight.That is why the approach works even on models with brutal training-data bias. The rules do not retrain the model. They just have to be specific enough and positioned right to outweigh the defaults.
Practical Implications
The token-probability mechanism has concrete consequences for anyone building with AI agents.
1. Prompting alone will not save you. "Be creative" or "use unusual colors" nudges the output slightly, then it snaps back to the same patterns. The training distribution is too strong. You need specific, concrete rules — start with an anti-slop prompt template.
2. Temperature is not your friend for design. Higher temperature buys variety at the cost of correctness. Keep it low and you get repetition; push it high and you get breakage.
3. Seeds beat dice. Sailop's procedural design system generator uses deterministic seeds to produce unique but reproducible systems. You can iterate on a seed and keep consistency — randomness gives you neither.
4. Constraints compose multiplicatively. "No blue" alone has a moderate effect. Ten rules together compound, because each one carves the distribution down again. The 73 rules in the manifesto work as a system, not a checklist.
5. It gets worse before it gets better. As more sites get built by agents, training data fills with AI-generated patterns and the model learns from its own output, reinforcing the dominant ones. That feedback loop means detecting AI-generated code gets both easier and more necessary every year.
How to Verify This Yourself
Run a simple experiment to make the bias visible:
# Generate 100 landing pages with default settings
for i in $(seq 1 100); do
echo "Build a landing page for a SaaS product called TestApp$i" | \
claude --print > "output_$i.html"
done
# Count background classes across all outputs
grep -oh "bg-[a-z]*-[0-9]*" output_*.html | sort | uniq -c | sort -rn | head -20Your counts will vary by model and version, and we are not printing a result we did not run. Look at which families lead the colored background classes: if the bias described above holds for your model, a handful of blues and grays will sit at the top. That is the training distribution made visible.
Conclusion
The sameness of AI-generated CSS is not a bug in any one model. It is a mathematical consequence of how language models work. Training-data bias produces skewed token distributions; standard sampling parameters cannot unskew them without breaking output. The only fix that holds is constraint-based generation — specific rules that steer the model toward intentionally different choices.
Sailop exists because we read the problem at the technical layer and built the tool at the right one: not more randomness in the model, but more constraints in the context. The output stays valid, stays consistent, and comes out unique.
The Sailop Claude Code skill and CLI that automate this are not published yet (the install page tracks them). Until then, a rules file in CLAUDE.md or .cursor/rules does the constraint injection by hand, and the free web scan shows what shipped.
# Once published
npx sailop install
sailop generate --seed "my-brand"
sailop skill install
# Now every Claude Code session produces unique CSSThe model has not changed. The constraints have.
Revision note
Updated 18 October 2026. An earlier version presented frequency charts for colors, fonts and layouts "counted across a sample of training data", a result of 100 runs where blue made up "over 60%" of colored backgrounds, and a "23 times" ratio between blue and fuchsia. We did not run those counts, so they were removed. The token-probability tables are kept to show the mechanism and are now labeled as illustrative, the public evidence is sourced, and the Sailop skill is described as not published yet.
Ship code that looks intentional
Scan any live page for AI-default patterns and get a fix kit: design tokens, class swaps and an agent brief. Free, no account.