How to Stop Cursor and Claude Code From Shipping Tailwind-Blue Slop
Ask Claude Code for a landing page and you get #3b82f6, Inter, and rounded-2xl in 40 seconds. Here are the four files and prompts that move the model off the statistical center for good.
Ask Claude Code to "build a landing page for my SaaS" and time how long it takes to produce a bg-blue-600 hero with an Inter font stack, a rounded-2xl card grid, and a button that says "Get Started." On my machine: about 40 seconds. The output is competent. It's also identical to the last 4,000 landing pages every other model produced from the same prompt — same #3b82f6, same from-blue-500 to-purple-600 gradient, same shadow-lg on hover.
This isn't a bug. It's the model doing exactly what it was trained to do: hit the statistical center of "good website" as defined by GitHub, Dribbble shots, and shadcn starter repos. The center is blue. Your job is to move the model off the center, on purpose, with config — not to nag it in chat every time.
Here's the practical version: why the defaults happen, and the exact files, prompts, and checks that stop them.
Why the model reaches for blue every time
A language model picks the next token by probability. When you say "primary button color" with no other signal, the highest-probability completion is blue because blue dominates the training corpus. Bootstrap shipped #007bff. Tailwind's docs use blue in nearly every example. shadcn/ui's default theme is a near-black-and-white neutral that everyone immediately themes to — guess what — blue. Stripe, Linear-clones, and every "AI SaaS" template lean blue-violet. The model has seen bg-blue-600 paired with the word "button" tens of millions of times and bg-[#c2410c] paired with it maybe a few thousand.
So the defaults are predictable:
- Color:
#3b82f6(blue-500) /#2563eb(blue-600), often with afrom-blue-X to-purple-Xgradient. I broke down exactly why this gradient is the tell in the Tailwind blue-purple gradient piece. - Type:
Inter,font-sans, or a raw system stack. Always. - Radius:
rounded-2xlon cards,rounded-lgon buttons. - Copy: "Get Started", "Supercharge your workflow", "Everything you need to ship faster."
- Layout: centered hero, three-column feature grid with Lucide icons in rounded squares, a
border border-gray-200pricing table.
None of these are wrong. They're just the mean. And the mean is now so recognizable that readers clock it in under 30 seconds — I catalogued the full checklist in detect an AI site in 30 seconds. Shipping the mean is shipping a "made by AI, didn't bother" signature.
The fix has four layers: a rules file the model reads on every task, a token file it's forced to pull from, a deleted color palette that breaks bad output at build time, and a check that fails the commit when blue leaks back in.
Layer 1: The rules file (CLAUDE.md / .cursorrules)
Both tools auto-load a project rules file. Claude Code reads CLAUDE.md from the project root and walks up parent dirs. Cursor reads .cursorrules (legacy) or .cursor/rules/*.mdc (current). This is the single highest-leverage change because it applies to *every* generation without you re-typing anything.
The mistake people make is writing aspirational fluff: "Make the design beautiful and unique." That does nothing — "beautiful" is also a high-probability blue page. You have to ban specific tokens and mandate specific replacements. Negative constraints plus positive defaults.
Here's a CLAUDE.md block that actually moves the needle:
## Design constraints (non-negotiable)
NEVER use these — they are the AI-default signature and are banned:
- Colors: blue-500/600/700 (#3b82f6, #2563eb), indigo-*, violet-* as a
PRIMARY or accent. No `from-blue-* to-purple-*` gradients, ever.
- Fonts: Inter, Roboto, the default system-ui stack, or `font-sans`
with no family defined.
- Radius: `rounded-2xl` on cards. Default card radius is `rounded-[3px]`.
- Copy: "Get Started", "Supercharge", "Everything you need",
"ship faster", "seamless", "unlock", "elevate", "10x".
ALWAYS pull design values from `src/styles/brand.ts`. Do not invent
hex codes. If a value you need is not in that file, STOP and ask.
Defaults when unspecified:
- Primary action color: var(--brand-ink) on var(--brand-paper)
- Body font: the `--font-body` token (currently "Söhne", fallback Georgia)
- Buttons: `rounded-none`, 1px solid border, no shadow
- Section rhythm: asymmetric. Do not center every hero.Two things make this work that the generic version misses.
First, it names the banned hex codes literally. The model can't rationalize #2563eb as "not really blue" if #2563eb sits in the ban list as a string. Second, it forces a lookup ("pull from brand.ts") instead of letting the model free-associate a color. A model asked to "use the brand color" with no source will still hallucinate blue. A model told "read this file line for line" uses what's in the file.
For Cursor's .mdc format, add frontmatter so the rule is always active:
---
description: Brand and anti-default design rules
alwaysApply: true
---
(same body as above)The alwaysApply: true matters. Without it Cursor treats the rule as opt-in, glob-matched against file paths, and the model never reads it on a fresh chat that hasn't touched a matching file yet.
Layer 2: The brand token file (the source of truth)
A rules file that says "use the brand color" with no brand color defined is theater. You need a real file the model reads and references. Keep it boring and machine-readable — a flat object, not a Tailwind config buried in extend.
src/styles/brand.ts:
// Single source of truth for design tokens.
// CLAUDE.md and .cursorrules both point here.
// Edit values here ONLY. Do not inline hex codes in components.
export const brand = {
color: {
ink: "#1a1614", // near-black, warm. primary text + buttons
paper: "#f4f1ea", // off-white, warm. page background
accent: "#c2410c", // burnt orange. links, focus, one CTA per page
muted: "#6b6259", // secondary text
line: "#d8d2c6", // hairline borders
},
font: {
// Self-hosted. Inter is banned.
display: '"GT Sectra", Georgia, serif',
body: '"Söhne", "Helvetica Neue", Arial, sans-serif',
mono: '"Berkeley Mono", ui-monospace, monospace',
},
radius: {
sharp: "0px",
soft: "3px", // the max. rounded-2xl is banned.
},
shadow: {
none: "none",
// one shadow, used sparingly. no shadow-lg pile-ups.
lift: "0 1px 0 #d8d2c6, 0 2px 8px rgba(26,22,20,0.06)",
},
} as const;Pick values that are *off* the AI mean by construction. Warm near-black #1a1614 instead of pure #000. Burnt orange #c2410c instead of blue. A 3px max radius instead of rounded-2xl. The point isn't that orange beats blue — it's that a deliberate, documented choice reads as human, and the statistical default reads as a robot. That's the thesis of from AI slop to signature: the differentiator isn't taste, it's *evidence of a decision*.
Wire it into Tailwind so the model has named utilities and no excuse to write raw blue:
// tailwind.config.ts (use .ts so the brand.ts import resolves —
// a .js config can't import a TS module without a loader)
import { brand } from "./src/styles/brand";
export default {
theme: {
// Replace, don't extend — kills the default blue-* scale entirely.
colors: {
transparent: "transparent",
current: "currentColor",
ink: brand.color.ink,
paper: brand.color.paper,
accent: brand.color.accent,
muted: brand.color.muted,
line: brand.color.line,
},
borderRadius: {
none: "0",
DEFAULT: brand.radius.soft,
},
fontFamily: {
display: brand.font.display.split(","),
body: brand.font.body.split(","),
},
},
};Using theme.colors instead of theme.extend.colors is the aggressive move: it *deletes* blue-500, indigo-600, and the rest of the default palette. Now if Claude Code writes bg-blue-600, it doesn't render — the class doesn't exist. The model gets fast negative feedback from its own broken output, which is far more reliable than hoping it read your rules file. Same monoculture-escape logic as the shadcn design monoculture piece: own your tokens, don't inherit the defaults everyone else inherits.
Layer 3: The prompt patterns
Rules files set the floor. The prompt sets the ceiling. Even with perfect config, a lazy prompt ("build a pricing section") gives you a lazy, mean-reverting result. Three patterns push back.
Pattern 1 — Constrain by reference, not adjective. Don't say "modern and clean." Say:
Build the pricing section. Use ONLY tokens from brand.ts.
Match the visual language of the existing Hero component in
src/components/Hero.tsx — same radius, same border treatment,
same type scale. Do not introduce new colors or a card shadow."Match Hero.tsx" anchors the model to *your* existing decision instead of the global average. It now has a local prior stronger than the training prior.
Pattern 2 — Forbid the cliché copy inline. The visual ban handles color; copy needs its own line every time, because "Get Started" is sticky:
Microcopy rules: no "Get Started", no "Supercharge", no verb-soup.
The CTA names the actual next action — e.g. "See the 14-day plan"
or "Check my address". Write like a person who has used the
product, not a marketer describing it.Pattern 3 — Make it justify deviations. Add one line that flips the model from "match the mean" to "defend the choice":
For any color, font, or radius you choose, add a one-line code
comment explaining why that token (not why it's pretty — why THAT
token vs the default). If you can't justify it, use the brand token.This is a lightweight version of the full anti-slop prompt template — worth lifting wholesale if you generate more than a page or two. The forcing function is the comment: a model that has to write // accent here because focus states need the warm orange, not ink is a model that stopped autopiloting.
Layer 4: The pre-commit check (the part everyone skips)
Rules and prompts reduce slop probabilistically. A check makes it deterministic. The model *will* regress — a fresh chat, a forgotten rules file, a snippet pasted from somewhere — and you want the regression caught before it's a commit, not after a reader pattern-matches your site as AI-built.
A grep-based check covers most of it. Put this in a script.
scripts/check-slop.sh:
#!/usr/bin/env bash
set -euo pipefail
FILES=$(git diff --cached --name-only --diff-filter=ACM \
| grep -E '\.(tsx?|jsx?|css|html)$' || true)
[ -z "$FILES" ] && exit 0
FAIL=0
flag() { echo "SLOP: $1"; FAIL=1; }
# Default blue / indigo / violet anywhere in changed files
if echo "$FILES" | xargs grep -nE \
'(bg|text|border|from|to|ring)-(blue|indigo|violet)-[0-9]' 2>/dev/null; then
flag "default blue/indigo/violet utility — use brand tokens"
fi
# Raw banned hex codes
if echo "$FILES" | xargs grep -niE \
'#(3b82f6|2563eb|6366f1|8b5cf6)' 2>/dev/null; then
flag "banned AI-default hex code"
fi
# Inter / unstyled font-sans
if echo "$FILES" | xargs grep -nE \
'Inter|font-family:\s*Inter|font-sans\b' 2>/dev/null; then
flag "Inter or bare font-sans — use --font-body"
fi
# rounded-2xl on anything
if echo "$FILES" | xargs grep -nE 'rounded-2xl' 2>/dev/null; then
flag "rounded-2xl — max radius is rounded (3px)"
fi
# Cliché copy
if echo "$FILES" | xargs grep -niE \
'Get Started|Supercharge|ship faster|seamless|10x your' 2>/dev/null; then
flag "cliché marketing copy"
fi
[ "$FAIL" -eq 1 ] && { echo "Commit blocked. Fix the slop above."; exit 1; }
echo "No slop detected."Wire it with Husky so it runs on every commit:
npx husky init
echo 'bash scripts/check-slop.sh' > .husky/pre-commitNow a bg-blue-600 that slipped past the rules file dies at git commit with a file and line number. Concretely, the output looks like this:
src/components/Pricing.tsx:42: <button className="bg-blue-600 ...
SLOP: default blue/indigo/violet utility — use brand tokens
Commit blocked. Fix the slop above.That closes the loop: config nudges, the deleted palette breaks bad output at build time, and the hook blocks anything that survives both. The grep list doubles as documentation — when a teammate trips SLOP: Inter or bare font-sans, they learn the rule without reading a wiki.
For the patterns grep can't catch — a centered-everything layout, the three-icon feature grid, a shadow-lg pile-up — you still need eyes. The 23 tells in how to spot AI-generated code make a decent manual checklist; run them once per page before you call it done.
Putting it together
The order matters, because each layer catches what the previous one misses:
brand.ts— define real, off-center tokens. Warm ink, one accent, 3px radius.- Tailwind
theme.colors(replace, not extend) — deleteblue-*so bad output literally won't render. CLAUDE.md+.cursorrules— ban the specific hex codes and copy by string; force a lookup tobrand.ts.- Prompt patterns — anchor to an existing component, forbid cliché copy, demand a justification comment.
check-slop.shpre-commit — fail the commit on any leaked blue, Inter,rounded-2xl, or "Get Started".
Set this up once per project and the marginal cost per generation is zero — you stop re-typing "please don't make it blue" into chat forever. The model still wants to drift to #3b82f6 on every task. You've just built four walls it can't drift through.
The deeper point: the tools aren't broken, and "AI design" isn't doomed to look the same. Cursor and Claude Code generate the average because you gave them no reason not to. Give them a documented, enforced point of view — a real token file, a hard ban list, a check that bites — and they'll generate *your* average instead of the internet's. The slop is a configuration default, and defaults are editable.
SHIP CODE THAT LOOKS INTENTIONAL
Scan your frontend for AI patterns. Generate a unique design system. Stop shipping the same blue gradient as everyone else.