| name | no-vibe-code |
| description | Stop shipping vibe-coded / AI-slop frontend. Use when building, designing, reviewing, or screenshotting any UI, landing page, web app, or component — to avoid the generated-default look (AI purple, purple→blue gradients, gradient clip-text, Inter everywhere, glassmorphism, emoji-as-icons, centered-hero + three cards, colored left-border cards, badge-above-H1, 01/02/03 step boxes, forced dark mode, untouched shadcn). Run slop-check.mjs to lint HTML/CSS/JSX for the tells before you call the UI done. |
no-vibe-code
Guardrail against "vibe-coded" frontend — UI that reads as generated by default
rather than designed on purpose. It has two parts that work together:
slop-check.mjs — a zero-dependency Node linter that scans your
HTML/CSS/JSX/TSX/Vue/Svelte for the specific fingerprints of AI slop and
fails the build if it finds high-severity ones. This is the primary tool.
Prose can't stop you shipping purple; a linter with a non-zero exit code can.
- The catalog below — the "do-not-ship" list with the exact value and the
fix for each tell. It's what the linter encodes, written out so you internalize
it while designing.
Paths below are relative to this skill dir (~/.claude/skills/no-vibe-code/).
When driving a real project, point the linter at that project's source with an
absolute path to the driver (see Run).
This skill is a linter + checklist, not an aesthetic. For positive direction
(deriving a distinctive look from the subject), pair it with the frontend-design
skill — that one tells you what to build; this one tells you what to never ship.
Prerequisites
Node ≥ 18. Nothing else — no install, no deps.
node --version
Run (agent path) — lint before you call the UI done
The loop: build the UI → run the linter at your source → fix every HIGH →
re-run until it PASSes → then screenshot. Don't declare a frontend done until
slop-check.mjs exits 0.
Point it at a project's source (absolute path to the driver so it works from any cwd):
node ~/.claude/skills/no-vibe-code/slop-check.mjs ./src
From inside this skill dir, the bundled samples demonstrate both outcomes:
node slop-check.mjs samples/slop.html
node slop-check.mjs samples/clean.html
Scan a whole directory (recurses; skips node_modules/.git/dist/build/.next):
node slop-check.mjs samples/
Flags:
node slop-check.mjs ./src --strict
node slop-check.mjs ./src --json
node slop-check.mjs ./src --quiet
Exit codes (use in CI or an agent self-check gate): 0 clean at failing
severity · 1 tells found · 2 bad args. Default fails on HIGH only; --strict
adds MEDIUM. LOW never fails the build — it's advisory.
Each finding prints SEV rule-id file:line, the reason, the matched snippet,
and the concrete fix. Sample HIGH line from samples/slop.html:
HIGH purple-blue-gradient samples/slop.html:18
Purple→blue gradient — the signature generated-landing-page fill.
match: from-purple…to-blue
fix: One intentional gradient max, in brand hues — or a solid color.
The catalog — what the linter flags, and why
Severity = how reliably the pattern outs a UI as generated. HIGH tells are near-
universal in "make me a beautiful landing page" output. Every one has an escape
hatch: if the brief genuinely calls for it, it's a choice, not slop — but you
must have chosen it.
Color (the loudest tells)
ai-purple-hex / ai-purple-class (HIGH) — "VibeCode purple." Detected by
computed hue (violet/indigo, 252–288°, saturated), not a fixed list, so it
catches near-purples too — plus Tailwind *-purple/violet/indigo/fuchsia-NNN.
Fix: pick a palette from the subject's own world before you write a color.
purple-blue-gradient (HIGH) — the purple→blue diagonal fill. Fix: one
intentional gradient max, in brand hues, or a solid.
gradient-cliptext (HIGH) — bg-clip-text + transparent + gradient headline.
Fix: solid high-contrast headline; let type and copy carry the hero.
gradients-everywhere (MEDIUM) — 4+ gradients in one file. Fix: one or two per view.
neon-glow (MEDIUM) — box-shadow: 0 0 40px <color>, shadow-purple-*,
drop-shadow-[0_0_…]. Fix: 0 2px 8px rgba(0,0,0,.08); drop colored glows.
forced-dark (LOW) — hard <html class="dark"> or bg-black/slate-950 root.
Fix: make light/dark a real choice; if dark, hit WCAG AA on body text.
Type
inter-font (HIGH) — Inter, "the Helvetica of the LLM era," the font nobody
chose. Fix: a display+body pair chosen for this brief.
overused-typepair (LOW) — Space Grotesk / Instrument Serif / Geist, the new
"tasteful default." Fix: fine once, but justify it — it's the current median.
all-caps-label (LOW) — uppercase / text-transform:uppercase eyebrows.
Fix: sentence case; reserve caps for one brand moment.
Layout
colored-left-border-card (HIGH) — the 3–4px colored left stripe on cards.
Fix: subtle outline, background tint, or a real shadow instead.
badge-above-h1 (MEDIUM) — a rounded-full pill stacked right on top of the
H1. Fix: cut it or fold it into the headline.
grid-cols-3-cards (MEDIUM) — the three-column card grid, other half of the
"centered hero + 3 cards" homepage. Fix: asymmetry; cards with differing roles/sizes.
step-number-boxes (LOW) — 01 / 02 / 03 boxes. Fix: number only real
sequences; else a timeline or connected flow.
stat-banner (LOW) — 10K+ / 99.9% / 3x trust-badge row. Fix: weave real
numbers into copy, or drop vanity stats.
Components / effects
emoji-as-icon (HIGH) — 🚀⚡🎯✨🔥… as iconography. Fix: one consistent
themed SVG set, or none.
glassmorphism (MEDIUM) — backdrop-filter: blur / backdrop-blur. Fix:
solid or subtly-varied backgrounds; blur only for real depth.
shadcn-default-radius (LOW) — --radius: 0.5rem, i.e. shadcn shipped
untouched. Fix: customize tokens — radius, shadows, primary — before deploy.
The three-move fix (highest leverage first)
When the linter lights up, the research is unanimous that these three moves alone
take a UI from "heavy slop" to "clean":
- Replace the palette — dump lavender, commit to one strong opinion.
- Change the typeface — anything deliberate that isn't Inter.
- Commit to one layout primitive and repeat it — kill the centered-hero-+-3-cards default.
Gotchas (battle scars)
- Purple is caught by computed hue, not a hex allowlist.
slop-check.mjs
parses each #rrggbb, converts to HSL, and flags hue 252–288° with enough
saturation. So #8b5cf6, #a855f7, and a hand-tweaked #7d4fe0 all trip it —
you can't dodge it by nudging the hex. If you get a false positive on a
legitimately-violet brand, that's a LOW-noise HIGH: eyeball it and move on.
badge-above-h1 is a whole-file rule, not per-line. The pill and the <h1
are almost always on different lines, so a per-line regex both missed the real
case and false-fired on any self-contained <h1>…</h1> line. It now scans the
full text for a rounded-full element within ~140 chars of an <h1. If you
restructure a hero across many lines, it may stop matching — that's fine.
- The emoji list is curated, not "all emoji." Matching every
\p{Extended_Pictographic} flagged legit emoji in body copy. The rule uses a
fixed set of the classic "AI icon" emoji (🚀⚡🎯✨🔥💡🎨📈📊🛡️🔒✅…). A novel
emoji-as-icon may slip through; add it to the set in slop-check.mjs.
- Low-confidence patterns are LOW severity on purpose (stat-banner, step
boxes, all-caps, forced-dark, shadcn radius). They're real tells but prone to
false positives, so they never fail the build — they inform, and you decide.
- Markdown isn't scanned, so linting docs is a no-op (
0 file(s) scanned).
Verified: node slop-check.mjs SKILL.md scans nothing even though this file is
full of from-purple, Inter, and emoji as examples — .md isn't in the
extension set. The flip side: the fixtures are scanned, so
node slop-check.mjs samples/slop.html is meant to FAIL — that's the
regression check, not a bug. Point the linter at your project's real source.
- A green linter is necessary, not sufficient. It catches the mechanical
tells. It can't see that your layout is boring or your copy is generic. Passing
slop-check.mjs clears the floor; frontend-design and your own screenshot are
how you clear the bar. Always look at the rendered result.
Troubleshooting
exit=2 / usage line — no target given. Pass a file or dir:
node slop-check.mjs ./src.
- "0 file(s) scanned" — the target had no scannable extensions
(.html/.css/.jsx/.tsx/.js/.ts/.vue/.svelte/.astro/.scss/.htm), or everything
was under a skipped dir (node_modules, dist, .next…). Point at the right folder.
- Legit violet brand keeps failing — it's a HIGH by design; confirm by eye,
and if the whole brand is genuinely violet, that's your one justified exception.
- Want it in CI — it already exits non-zero on HIGH; add
node ~/.claude/skills/no-vibe-code/slop-check.mjs ./src (or --strict) as a
lint step.
The driver
slop-check.mjs lives in this skill dir. It's committed agent tooling — extend
it: add a rule to the RULES array (per-line test) or a whole-file check in
scanFile. samples/slop.html (should FAIL) and samples/clean.html (should
PASS) are the regression fixtures — run both after any change to the rules.
Basis
Rules are drawn from field observation of AI-generated UI, cross-checked against:
the official frontend-design skill's calibration notes; a catalog of 16 AI
"design slop" patterns (developersdigest.tech); and Reddit-mined rankings of
vibe-coded tells (github.com/JCarterJohnson/vibecoded-design-tells), where
shadcn/Tailwind defaults, AI purple, gradient hero text, and centered-hero-+-3-cards
rank as the top giveaways.