All tools
SkillMember post

Frontend Engineer

by sanjaesureshUpdated Jul 5, 2026

The disciplined build/fix loop for frontend work — UI components, pages, web apps, dashboards, landing pages. Like software-engineer, but specialized for the browser: it bakes in not-AI-looking design (reference-first brief + the vibe-coded-tells catalog and a CI scanner) AND production-grade engineering gates (WCAG 2.2 AA accessibility, Core Web Vitals / INP performance, every-state coverage, semantic HTML, forms UX, responsive, design tokens, testing). Use whenever you are building, styling, or changing any web UI and want it to look human-made and hold up in production — not just render in a demo. Trigger on "build a page/component/dashboard/landing page", "make this UI", "frontend", "doesn't look AI-generated", "production-grade UI". Attribution: The bundled tell-catalog (ai-tells-catalog.md) and devibe_scan.py are adapted from Carter Johnson's vibecoded-design-tells (MIT License).

View on GitHub

What it does

  • /frontend-engineer

    The disciplined build/fix loop for frontend work — UI components, pages, web apps, dashboards, landing pages.

Files (3)

SKILL.mdprimary · markdown · 9.0 KB
---
name: frontend-engineer
description: >-
  The disciplined build/fix loop for frontend work — UI components, pages, web apps,
  dashboards, landing pages. Like software-engineer, but specialized for the browser: it
  bakes in not-AI-looking design (reference-first brief + the vibe-coded-tells catalog and
  a CI scanner) AND production-grade engineering gates (WCAG 2.2 AA accessibility, Core Web
  Vitals / INP performance, every-state coverage, semantic HTML, forms UX, responsive,
  design tokens, testing). Use whenever you are building, styling, or changing any web UI
  and want it to look human-made and hold up in production — not just render in a demo.
  Trigger on "build a page/component/dashboard/landing page", "make this UI", "frontend",
  "doesn't look AI-generated", "production-grade UI".
---

# frontend-engineer

The default loop for building or changing **frontend** code. It is the browser-facing
counterpart to `software-engineer`: same discipline (understand → plan → small steps →
verify with evidence → self-review), plus the two things that separate production UI from
a pretty demo — **it doesn't read as AI-generated**, and **it handles accessibility,
performance, and every state**, not just the happy path.

A working render is not done. Done is: a deliberate look, keyboard-operable, contrast-safe,
fast (LCP/INP/CLS in budget), and correct in its empty/loading/error/overflow states.

## When to use something else

- Backend / non-UI build or fix → `/software-engineer`.
- Pure visual de-slop **audit** of existing code, or "does this look AI" → run this skill's
  bundled `scripts/devibe_scan.py` (the Verify step below) for the review-only pass. The
  standalone `unslop-ui` skill does the same audit with a fuller catalog if you have it
  installed — it is optional and not bundled here.
- Open-ended *creative* direction / "make it beautiful from scratch" → `/frontend-design`
  or `/impeccable`; then return here to engineer it properly.
- Reviewing a UX/frontend plan before code → `/design-plan-review`.
- Reviewing a frontend diff → `design-reviewer` agent or `/pre-pr-review`.

This skill is the implementer that pulls those in at the right moment. It is the only
frontend skill that combines deliberate-look enforcement with the engineering gates.

## The loop

### 1. Brief before CSS (this is where "looks AI" is won or lost)

Most "looks AI" outcomes are a *specification* problem, not a styling one. An unspecified
prompt returns the median of the training data, and everyone's median is identical. Before
generating UI, establish the brief — pull it from the user, or state the choices you are
making and why. Do not silently fall back to defaults.

Establish concretely (method in [references/choosing-a-look.md](references/choosing-a-look.md)):

- **A reference.** One real site, brand, or screenshot whose design language to follow.
  This single input does more than every other rule combined. If none exists, commit to a
  *named direction* (editorial, brutalist, utilitarian-dense, warm-consumer, technical-mono)
  — never "modern and clean."
- **A color decision.** A real or deliberately chosen brand color, stated. Not the
  framework indigo/violet default, and not the cream/sage "tasteful" default either.
- **A type decision.** A specific typeface/pairing with a reason. Avoid the autopilot picks
  (Inter, Geist; and Instrument Serif, Fraunces, Playfair) unless they are a real choice.
- **A layout intent.** What the page is *for* and what the user should do first — this is
  how you avoid the centered-hero + three-feature-cards skeleton. Structure follows goal.

### 2. Build, avoiding the tells

While building, avoid the specific signatures in
[references/ai-tells-catalog.md](references/ai-tells-catalog.md) (data-ranked from ~3.2M
Reddit posts). The top ones: default shadcn/Tailwind look, AI purple, gradient text, motion
on everything, rounded-everything, unprompted neon glow, emoji-as-icons, generic fonts, the
hero+3-cards skeleton, and the cream+serif+sage "tasteful default." A tell is an
*unspecified default*, not a banned value — if the user genuinely chose purple or cream as a
brand decision, that is not slop; leave it and mark `unslop-ignore`.

The trap: don't fix one default by reaching for another (`bg-purple-600` →
`bg-emerald-700` is not unslopping, it just resets the clock). Apply the project's actual
choice from step 1.

### 3. Engineer it as you build (not as a cleanup pass)

These are gates, not nice-to-haves. Pull the rules from the references and apply them while
writing the component, because retrofitting accessibility and state handling is far more
expensive than building them in:

- **Semantic HTML + accessibility** → [references/accessibility.md](references/accessibility.md).
  Native element for the job (`<button>` for actions, `<a href>` for nav, `<dialog>` for
  modals), keyboard-operable with a visible focus ring, ARIA only for true custom widgets,
  contrast ≥ 4.5:1 / 3:1.
- **Every state** → [references/states-and-forms.md](references/states-and-forms.md).
  Loading (skeleton vs spinner), empty, error+retry, zero/one/many, long content, offline.
  Forms: validate on blur, specific error messages, correct `autocomplete`/`inputmode`,
  focus the first invalid field.
- **Performance** → [references/performance.md](references/performance.md). LCP element
  prioritized (never lazy-loaded), no >50ms long tasks, dimensions on all media, fonts with
  `swap` + preload, animate only `transform`/`opacity`.
- **Architecture + tokens** → [references/architecture-and-tokens.md](references/architecture-and-tokens.md).
  No derived state in `useEffect`, `"use client"` at leaves, semantic design tokens (no
  hardcoded hex/px), no secrets across the server→client boundary.
- **Production readiness** (for the surfaces that need it) →
  [references/production-readiness.md](references/production-readiness.md). SEO/metadata
  and social cards on public pages, security headers + CSP + SRI, the image/media
  pipeline, real-user observability, and the CI gates (`eslint-plugin-jsx-a11y`, axe,
  Lighthouse budgets) that actually enforce the rules above.

### 4. Verify before declaring done

Evidence-gated, like the rest of the toolkit. Run the checks; don't claim what you didn't
verify. When unsure between DONE and UNVERIFIABLE, say UNVERIFIABLE.

```bash
# From this skill's directory. Exit code = high-severity count → CI can gate on it.
python3 scripts/devibe_scan.py <path>                  # full report + vibe score
python3 scripts/devibe_scan.py <path> --severity high  # strongest signals only
python3 scripts/devibe_scan.py <path> --json           # machine-readable for CI
```

The scanner catches the mechanical tells (colors, fonts, gradients, the cream+serif combo).
It **cannot** see layout coherence, spacing consistency, focus order, or whether text
overflows — check those by eye and by keyboard.

## Definition of done (frontend)

Not done until every line is true (or explicitly N/A with a reason):

- [ ] **Deliberate look** — color, type, and layout each trace to a stated reason, not a
      default. `devibe_scan.py` high-severity count is 0 (or every finding is a justified
      `unslop-ignore`).
- [ ] **Keyboard** — full flow operable with the mouse unplugged; visible focus ring
      everywhere; focus managed on modal open/close and route change; no traps.
- [ ] **Contrast** — body ≥ 4.5:1, UI/large text ≥ 3:1, verified in light *and* dark.
- [ ] **Semantics** — right native element for each role; one `<main>`/`<h1>`; headings
      don't skip; ARIA only where a native element couldn't do it.
- [ ] **States** — loading, empty, error+retry, zero/one/many, long-content, and offline
      all handled and seen. Forms validate on blur with specific, linked error messages.
- [ ] **Core Web Vitals** — LCP element prioritized; no obvious >50ms main-thread tasks;
      all media has dimensions; fonts load without invisible-text or swap-shift.
- [ ] **Responsive** — works at ~320px, tablet, desktop, and 200% zoom; tap targets
      ≥ 24×24px; no horizontal scroll or clipping.
- [ ] **Tokens** — spacing/color/type/radius reference tokens; no stray hardcoded hex/px.
- [ ] **Tests** (when the project has a suite) — behavior queried by role/label; axe run on
      the primary flow; critical user flow has an E2E.
- [ ] **`prefers-reduced-motion`** honored; motion only where it signals causality.
- [ ] **Production readiness** (where it applies) — public pages have unique title/meta +
      canonical + OG card; third-party scripts use SRI; `target="_blank"` has
      `rel="noopener"`; images use modern formats with correct `srcset`/`sizes`; a CI gate
      (`eslint-plugin-jsx-a11y`/axe/Lighthouse) enforces the above.

For the full highest-leverage list, the references carry the thresholds and the sources.

## Attribution

The tell catalog and `devibe_scan.py` are vendored from
[vibecoded-design-tells](https://github.com/JCarterJohnson/vibecoded-design-tells) (Carter
Johnson, MIT) — see [ATTRIBUTION.md](ATTRIBUTION.md). The engineering reference files are
original to this skill.
devibe_scan.pypy · 11.3 KB
#!/usr/bin/env python3
"""
devibe_scan.py - scan a web codebase for "vibe-coded" / AI-generated design tells.

Plain Python, standard library only. Detection patterns and their severity come from a
Reddit analysis (~3.2M posts / 3,033 on-topic comments across 47 subreddits) of what
people actually flag as making a site look AI-generated. Severity follows how often each
tell is named in that data, so the report tells you where to spend effort.

Usage:
    python3 devibe_scan.py <path>                 # scan a dir or file
    python3 devibe_scan.py <path> --severity high # only high-signal tells
    python3 devibe_scan.py <path> --json          # machine-readable (for CI)
    python3 devibe_scan.py <path> --max 8         # cap examples shown per rule

Exit code is the number of HIGH-severity findings (0 = none), so CI can gate on it.
"""
import os, re, sys, json, argparse

EXTS = {".html", ".htm", ".css", ".scss", ".sass", ".less", ".js", ".jsx",
        ".ts", ".tsx", ".vue", ".svelte", ".astro", ".mdx"}
SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", "out", "vendor",
             "coverage", ".svelte-kit", ".astro", ".turbo", ".cache", "__pycache__"}
W = {"high": 3, "medium": 2, "low": 1}

# Each rule: id, label, severity, fix, and patterns (compiled case-insensitive).
# Keep patterns specific enough to avoid drowning the report in false positives.
RULES = [
    # ---- HIGH: the top concrete tells ----
    {"id": "shadcn-default-card", "label": "Untouched shadcn default Card / theme", "sev": "high",
     "fix": "Theme the tokens (primary, radius, neutrals, spacing). Stock defaults are the giveaway, not shadcn.",
     "pats": [r"rounded-lg\s+border\s+bg-card\s+text-card-foreground\s+shadow-sm",
              r"\"baseColor\"\s*:\s*\"(slate|zinc|gray|neutral|stone)\"",
              r"--radius\s*:\s*0\.5rem"]},
    {"id": "ai-purple", "label": "AI purple / indigo / violet as primary color", "sev": "high",
     "fix": "Pick a brand color outside the violet/indigo/purple band. It is Tailwind's default, so it reads as 'nobody chose this'.",
     "pats": [r"\b(bg|text|from|via|to|border|ring|fill|stroke|decoration|outline)-(indigo|violet|purple|fuchsia)-(400|500|600|700|800)\b",
              r"#(6366f1|4f46e5|818cf8|7c3aed|6d28d9|8b5cf6|a855f7|9333ea|7e22ce|c026d3|d946ef)\b"]},
    {"id": "gradient-text", "label": "Gradient-filled text (heading/hero)", "sev": "high",
     "fix": "Solid color on headings and copy. Gradient body text is one of the strongest AI tells.",
     "pats": [r"bg-clip-text\s+[^\"'`]*text-transparent", r"text-transparent\s+[^\"'`]*bg-clip-text",
              r"-webkit-background-clip\s*:\s*text", r"\bbackground-clip\s*:\s*text"]},
    {"id": "purple-blue-gradient", "label": "Purple-to-blue/pink gradient", "sev": "high",
     "fix": "Default to solid fills. If you must gradient, keep stops analogous and low-contrast, never the rainbow purple-to-blue.",
     "pats": [r"from-(purple|violet|indigo|fuchsia)-\d+\s+(via-[a-z]+-\d+\s+)?to-(blue|indigo|pink|cyan|sky)-\d+",
              r"linear-gradient\([^)]*#(6366f1|7c3aed|8b5cf6|a855f7)[^)]*\)"]},
    {"id": "claude-default-look", "label": "The 'tasteful default' look (cream background + serif display)", "sev": "high",
     "fix": "This is the 2026 tell, not the fix. Anchor color and type to the real brand or a reference. If cream + serif is a genuine decision, mark the line unslop-ignore.",
     "pats": [r"#(faf8f5|f5f1e8|f3eee3|fdfbf7|f7f3ec|faf6ef|f6f1e7|fbf7f0|f4efe4)\b",
              r"\bbg-(stone|amber|orange)-(50|100)\b",
              r"\b(Instrument\s*Serif|Fraunces|Playfair\s*Display|Cormorant|Spectral|DM\s*Serif)\b"]},

    # ---- MEDIUM ----
    {"id": "hero-three-cards", "label": "Centered hero + three-feature-card grid skeleton", "sev": "medium",
     "fix": "Break the grid. Asymmetric hero with a real screenshot; vary sections instead of stacked 3-up icon cards.",
     "pats": [r"grid-cols-1\s+(sm:grid-cols-2\s+)?md:grid-cols-3"]},
    {"id": "rounded-everything", "label": "Large rounded corners / pill buttons everywhere", "sev": "medium",
     "fix": "Use a small, intentional radius scale by role. Not everything maximally rounded; pills only occasionally.",
     "pats": [r"\brounded-(2xl|3xl|full)\b", r"border-radius\s*:\s*(999\d*px|9999px)"],
     # rounded-full on a small sized box is a status dot / avatar / icon, not a pill button. Skip those.
     "suppress": r"\b[hw]-(\d|10|11|12|14|16)(\.5)?\b"},
    {"id": "fade-in-animations", "label": "Boilerplate fade-in / hover-grow / scroll animation", "sev": "medium",
     "fix": "Motion only when it communicates something; gate behind prefers-reduced-motion. Minor tell, noisier signal.",
     "pats": [r"initial=\{\{\s*opacity:\s*0", r"whileInView", r"whileHover=\{\{\s*scale",
              r"data-aos\s*=", r"\bhover:scale-1\d{2}\b"]},
    {"id": "neon-glow", "label": "Unprompted neon glow shadow", "sev": "medium",
     "fix": "Remove glow you did not deliberately design. Dark mode should rely on contrast, not glow.",
     "pats": [r"shadow-\[0_0_", r"drop-shadow-\[0_0_", r"text-shadow\s*:[^;]*\d+px[^;]*(rgba|#|hsl)",
              r"box-shadow\s*:[^;]*\b0\s+0\s+\d{2,}px"]},
    {"id": "emoji-as-icons", "label": "Emoji used as icons / section bullets", "sev": "medium",
     "fix": "Use a real SVG icon set (Lucide/Phosphor/Heroicons) or none. Emoji-as-UI signals low effort.",
     "pats": [r"[\U0001F680✨⚡\U0001F525\U0001F4A1\U0001F512✅\U0001F3AF\U0001F31F\U0001F6E1\U0001F4C8\U0001F511\U0001F389\U0001F680]"]},
    {"id": "generic-font", "label": "Generic default font (Inter / Geist / Roboto / system)", "sev": "medium",
     "fix": "Choose a typeface with character and pair a display + body face. The starter font reads as 'no choice made'.",
     "pats": [r"font-family\s*:\s*['\"]?(Inter|Geist|Roboto)\b",
              r"\b(Inter|Geist|Geist_Mono|Roboto)\s*\(",
              r"fontFamily\s*:\s*\{[^}]*['\"](Inter|Geist|Roboto)"]},

    # ---- LOW: copy + minor ----
    {"id": "hype-copy", "label": "Generated marketing copy cliche", "sev": "low",
     "fix": "Say what the product literally does, with specifics. Cut the template hype words.",
     "pats": [r"\bTransform your\b", r"\bSupercharge\b", r"\bUnleash\b", r"\bEffortlessly\b",
              r"\breimagined\b", r"take your [^.]{0,30}to the next level", r"\bGame-?changer\b"]},
    {"id": "stock-illustration", "label": "Generic blob / stock illustration source", "sev": "low",
     "fix": "Use real screenshots or commissioned art instead of undraw-style blobs.",
     "pats": [r"undraw", r"storyset", r"\bdrawkit\b"]},
]

def compile_rules(min_sev):
    order = ["high", "medium", "low"]
    floor = order.index(min_sev) if min_sev else len(order) - 1
    out = []
    for r in RULES:
        if order.index(r["sev"]) > floor:
            continue
        r = dict(r)
        r["rx"] = [re.compile(p, re.IGNORECASE) for p in r["pats"]]
        r["suppress_rx"] = re.compile(r["suppress"], re.IGNORECASE) if r.get("suppress") else None
        out.append(r)
    return out

def iter_files(path):
    if os.path.isfile(path):
        yield path; return
    for root, dirs, files in os.walk(path):
        dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
        for f in files:
            if f.endswith(".min.js") or f.endswith(".min.css"):
                continue
            if os.path.splitext(f)[1].lower() in EXTS:
                yield os.path.join(root, f)

def scan(path, min_sev):
    rules = compile_rules(min_sev)
    findings = []
    for fp in iter_files(path):
        try:
            with open(fp, "r", encoding="utf-8", errors="ignore") as fh:
                lines = fh.readlines()
        except Exception:
            continue
        if len(lines) == 1 and len(lines[0]) > 5000:   # likely minified
            continue
        for i, line in enumerate(lines, 1):
            if "unslop-ignore" in line.lower():     # respect intentional choices
                continue
            for r in rules:
                if r["suppress_rx"] and r["suppress_rx"].search(line):
                    continue
                for rx in r["rx"]:
                    m = rx.search(line)
                    if m:
                        findings.append({"rule": r["id"], "label": r["label"], "sev": r["sev"],
                                         "fix": r["fix"], "file": fp, "line": i,
                                         "snippet": line.strip()[:160]})
                        break
    return findings

def verdict(by_sev, weighted):
    if by_sev.get("high", 0) >= 3 or weighted >= 15:
        return "STRONG AI-default look"
    if by_sev.get("high", 0) >= 1 or weighted >= 6:
        return "Some AI defaults present"
    if weighted > 0:
        return "Mostly clean, minor tells"
    return "Clean, no tells detected"

def main():
    ap = argparse.ArgumentParser(description="Scan a web codebase for vibe-coded design tells.")
    ap.add_argument("path")
    ap.add_argument("--severity", choices=["high", "medium", "low"], default="low",
                    help="minimum severity to report (default: low = everything)")
    ap.add_argument("--json", action="store_true", help="machine-readable output")
    ap.add_argument("--max", type=int, default=10, help="max examples shown per rule (text mode)")
    args = ap.parse_args()

    if not os.path.exists(args.path):
        print(f"path not found: {args.path}", file=sys.stderr); sys.exit(2)

    findings = scan(args.path, args.severity)
    by_sev = {}
    by_rule = {}
    for f in findings:
        by_sev[f["sev"]] = by_sev.get(f["sev"], 0) + 1
        by_rule.setdefault(f["rule"], []).append(f)
    weighted = sum(W[s] * n for s, n in by_sev.items())
    files_scanned = sum(1 for _ in iter_files(args.path))

    if args.json:
        print(json.dumps({"path": args.path, "files_scanned": files_scanned,
                          "counts": by_sev, "vibe_score": weighted,
                          "verdict": verdict(by_sev, weighted), "findings": findings}, indent=2))
        sys.exit(by_sev.get("high", 0))

    sev_order = {"high": 0, "medium": 1, "low": 2}
    rule_ids = sorted(by_rule, key=lambda rid: (sev_order[by_rule[rid][0]["sev"]], -len(by_rule[rid])))
    print(f"\n  unslop-ui scan: {args.path}")
    print(f"  files scanned: {files_scanned}   findings: {len(findings)}   vibe score: {weighted}")
    print(f"  verdict: {verdict(by_sev, weighted)}")
    print(f"  high: {by_sev.get('high',0)}   medium: {by_sev.get('medium',0)}   low: {by_sev.get('low',0)}\n")
    if not findings:
        print("  Nothing flagged. Either it is clean or the tells are layout/motion ones a regex"
              "\n  cannot see. Eyeball the hero layout and animations against references/tells.md.\n")
        return
    for rid in rule_ids:
        items = by_rule[rid]
        f0 = items[0]
        tag = f0["sev"].upper()
        print(f"  [{tag}] {f0['label']}  ({len(items)} hit{'s' if len(items)!=1 else ''})")
        print(f"        fix: {f0['fix']}")
        for it in items[:args.max]:
            print(f"        {it['file']}:{it['line']}  {it['snippet']}")
        if len(items) > args.max:
            print(f"        ... +{len(items) - args.max} more")
        print()
    top = [by_rule[rid][0]['label'] for rid in rule_ids[:3]]
    print("  Top things to change: " + "; ".join(top))
    print("  Layout and motion tells need eyes too. See references/tells.md.\n")
    sys.exit(by_sev.get("high", 0))

if __name__ == "__main__":
    main()
ai-tells-catalog.mdmarkdown · 18.0 KB
# The vibe-coded tells: full catalog

Each entry has the data evidence (so you can weight it), a real quote from the threads,
the code-level signatures the scanner keys on, and the fix. Ordered by priority (comment
share in the on-topic data, which is the cleanest signal). Source: ~3.2M posts across 47
AI/SaaS subreddits and 3,033 comments from 125 "why do AI sites all look the same"
threads, 2020 to 2026.

A note on weighting. The Reddit data was collected through mid-2026 and the loudest
single tell in it is the *old* default (purple gradient, dark hero). But defaults move.
The fastest-rising tell now is the "tasteful default" the previous generation of
anti-slop advice created, so it leads this catalog even though its raw count in the
historical data is still climbing. Treat tell 0 and tell 1 as co-top-priority.

## Contents

0. The new "tasteful default" (cream + serif + sage) — the 2026 tell
1. Default shadcn / Tailwind look
2. AI purple (violet / indigo primary)
3. Gradients everywhere / gradient text
4. Too many animations
5. Rounded corners on everything
6. Dark mode + neon glow
7. Emoji as icons
8. Generic sans fonts (Inter / Geist / Instrument Serif / Fraunces)
9. The hero + three feature cards + CTA skeleton
10. Layout-quality tells (overflow, spacing, alignment)
11. Lower-signal and copy tells
12. Cleared by the data (do not chase)

---

## 0. The new "tasteful default" (cream + serif + sage)

**Why it leads:** this is the look the previous wave of anti-slop advice (including
Claude's own frontend-design skill) converged on, so it is now the single most
recognizable "AI tried to be tasteful" signal. Reddit clocks it instantly and calls it
out by name: a warm cream or beige background, a serif display font (Instrument Serif,
Fraunces, Playfair), and a sage or forest green accent, often with a generated product
"screenshot" card on the right. It reads as AI for the same reason the purple gradient
did: nobody chose it, the model did.

**Why people react so strongly:** it is dishonest slop. The purple gradient at least
looked like a default. This one looks like taste, so being told it is also a default
lands harder. Comments single out "the beige and green theme alone is a dead giveaway,"
"piss colored background copied from Anthropic branding," and "Instrument Serif is a
top-5 vibecoded title font."

**Code signatures (scanner):**
- Cream/beige page background: `#faf8f5`, `#f5f1e8`, `#f3eee3`, `#fdfbf7`, `#f7f3ec`,
  Tailwind `bg-stone-50/100`, `bg-amber-50`, `bg-orange-50` as the *page* background.
- Serif display font: `Instrument Serif`, `Fraunces`, `Playfair Display`, `Spectral`,
  `Cormorant`, `DM Serif` used for headings.
- Sage/forest green primary: `#15573a`, `#1a4d3a`, hues around emerald/green 700-900 as
  the brand color, especially paired with the cream background.
- The combination of any two of those three is the strong signal.

**Fix:** the fix is not "use a different nice palette," because that is how you got here.
Anchor the look to the actual brand or a real reference, and if there is none, pick a
direction that is specific and uncommon rather than the current tasteful average. If the
project genuinely is a warm editorial brand and cream + serif is a real decision, mark it
`unslop-ignore` and move on. The tell is reaching for it because it is what "good" auto-
completes to.

---

## 1. Default shadcn / Tailwind look

**Evidence:** named in 2.5% of on-topic comments, the single most-cited concrete cause
of the "they all look the same" reaction. Independent commenters across 6+ subreddits.

**Quote:** "Every Claude/Cursor project defaults to the same shadcn components with
identical slate gray cards, that specific blue accent, and the same padding rhythm. You
can spot it from a screenshot." (r/ClaudeAI). Also: "It is because everyone use shadcn/ui."

**Why it reads as AI:** shadcn/ui and Tailwind are excellent, but their *defaults* are
the most common output a model produces. The slate/zinc/gray neutral cards, the default
ring/border, the uniform `p-6` padding rhythm, and the stock `rounded-lg` are a
fingerprint. shadcn is not the problem; shipping it untouched is.

**Code signatures (scanner):**
- Tailwind neutrals left as the card surface everywhere: heavy repeated `bg-slate-*`,
  `bg-zinc-*`, `bg-gray-*` on `Card`/`div` with `border` + `rounded-lg` + `shadow-sm`.
- Default shadcn New York / default theme tokens unedited: `--primary`, `--ring`,
  `--radius: 0.5rem` left at generated values; `components.json` present with default
  `baseColor: "slate"` or `"zinc"` and `cssVars` untouched.
- The trio `rounded-lg border bg-card text-card-foreground shadow-sm` (the stock Card)
  repeated many times with no theming.

**Fix:** override the theme before building. Set a real `--primary` (not the default),
a deliberate `--radius`, a custom neutral ramp, and your own spacing rhythm. Point
people at theme generators (for example tweakcn) if they want a fast non-default token
set. The test: could someone tell your Card from the shadcn docs Card in a screenshot?
If not, you have not themed it.

---

## 2. AI purple (violet / indigo primary)

**Evidence:** named in 2.3% of comments, the top color tell, and it co-occurs with
"gradients" more than any other pair (32 times). Commenters trace it directly to
Tailwind's default indigo.

**Quote:** "this purple is used way too much everywhere." And: "That black/purple theme
80% of the time for me." And from Anthropic's own frontend-design guidance, quoted by a
user: avoid "cliched color schemes (particularly purple gradients on white background)."

**Why it reads as AI:** Tailwind's default-ish accent and a lot of starter templates land
on indigo/violet (#6366f1, violet-500/600). Models reach for it when no brand color is
given, so purple-as-primary is a strong "nobody chose this" signal.

**Code signatures (scanner):**
- Tailwind: `indigo-*`, `violet-*`, `purple-*`, `fuchsia-*` used as the primary/CTA/link
  color (not just an accent dot).
- Hex/HSL: `#6366f1`, `#7c3aed`, `#8b5cf6`, `#a855f7`, `#6d28d9` and neighbors as primary.
- CSS vars: `--primary`/`--brand` set to a violet/indigo hue (HSL hue ~255 to 280).

**Fix:** choose a brand color that is not in the violet/indigo/purple band, or if purple
is genuinely the brand, make it a specific, off-default purple and pair it with a
non-default neutral so it does not read as the Tailwind swatch. The point is a chosen
color, not the default one.

---

## 3. Gradients everywhere / gradient text

**Evidence:** named in 2.0% of comments. The single highest-scored design comment in the
entire dataset (373 upvotes) leads with it.

**Quote:** "the purple-to-blue gradient is the biggest tell lol. also bento grid layouts,
rounded corners on everything, hero section with gradient text that says 'transform your
X', and way too much whitespace." (r/ClaudeAI, 373 upvotes)

**Why it reads as AI:** the purple-to-blue (or purple-to-pink) gradient on the hero,
gradient-filled headings, and gradient buttons are a template default. Gradient *body
text* especially screams generated, because almost no deliberate brand does it on real
copy.

**Code signatures (scanner):**
- Gradient text: `bg-gradient-to-* ... bg-clip-text text-transparent`, or CSS
  `background: linear-gradient(...); -webkit-background-clip: text`.
- Purple-to-blue/pink gradients: `from-purple-* to-blue-*`, `from-violet-* to-indigo-*`,
  `from-indigo-* to-pink-*`; CSS `linear-gradient(...#6366f1...#3b82f6...)`.
- Gradient on many surfaces: repeated `bg-gradient-to-*` across hero, buttons, and cards.

**Fix:** default to solid fills. Allow at most one restrained gradient as an accent (and
prefer analogous, low-contrast stops over the rainbow purple-to-blue). Never put a
gradient on running headings or paragraph text. A single solid, confident brand color
beats a gradient almost every time.

---

## 4. Too many animations (fade-ins, hover-grow, parallax, scrolljacking)

**Evidence:** named in ~1.1% of comments. Flagged as a real but **minor and noisier**
signal (the keyword catches tool mentions and positive notes too), so weight it below
the color and layout tells.

**Quote:** "It's always the unnecessary hover animations and gradients that give it away
in my opinion." Also: "What triggers me is the grow animation on hover." And complaints
about scrolljacking (the page hijacking your scroll to play transitions).

**Why it reads as AI:** generated sites tend to bolt fade-in-on-scroll onto every section
and a scale-up on every card hover, because the starter components include it. The motion
is decorative, uniform, and unmotivated.

**Code signatures (scanner):**
- Framer Motion fade/scale boilerplate: `initial={{ opacity: 0, y: 20 }}` /
  `whileInView` / `whileHover={{ scale: 1.05 }}` repeated across sections.
- Tailwind/AOS: `data-aos="fade-up"` everywhere, `animate-*` on most sections,
  `hover:scale-105` on every card.
- Scrolljacking libraries wired to the whole page.

**Fix:** use motion only when it communicates state or guides attention, and make it the
exception, not the wrapper around every element. Always honor `prefers-reduced-motion`.
If every section animates the same way, none of it means anything; cut it.

---

## 5. Rounded corners and pill buttons on everything

**Evidence:** named in 0.8% of comments, and it appears in the top-scored comment's list.

**Quote:** "The blue and purple with the rounded boxes looks very vibecoded to me." And:
"Purple gradients and the urge to put a box around everything."

**Why it reads as AI:** one large radius applied uniformly (cards, inputs, buttons,
images, the whole page) plus fully-pill buttons is a default. Real design uses radius
intentionally, often a smaller scale, and varies it.

**Code signatures (scanner):**
- `rounded-2xl` / `rounded-3xl` / `rounded-full` applied broadly to cards and containers.
- Pill buttons: `rounded-full` on every button.
- CSS: a single large `border-radius` token reused everywhere; `border-radius: 9999px`
  on buttons.

**Fix:** define a small radius scale and apply it on purpose. Not everything needs to be
maximally rounded. Sharp or lightly-rounded corners often read as more deliberate. Pill
buttons are fine occasionally, not as the only button shape.

---

## 6. Dark mode with neon glow (added unprompted)

**Evidence:** named in 0.7% of comments, multi-author and multi-subreddit, several noting
the model adds the glow even when never asked.

**Quote:** "AI loves this glowing shit. for no reason. but at least there is no purple
gradients. it's something." (r/webdev)

**Why it reads as AI:** dark background plus neon `text-shadow`/`box-shadow` glow on
headings, buttons, or borders, unprompted, is a generated-template default. Dark mode
itself is fine; the unrequested glow is the tell.

**Code signatures (scanner):**
- Glow shadows: `shadow-[0_0_*]`, large colored `box-shadow` / `text-shadow` with low
  blur spread and a saturated color, `drop-shadow-[0_0_*]`.
- Neon-on-dark combos: bright `text-cyan-400`/`text-green-400`/`text-fuchsia-400` on
  `bg-black`/`bg-slate-950` with glow.

**Fix:** remove glow you did not deliberately design. If the brand is genuinely
neon/cyberpunk, keep it sparing and intentional. Default dark mode should rely on
contrast and spacing, not glow.

---

## 7. Emoji used as icons or section bullets

**Evidence:** named in 0.5% of comments. Verified as real specifically as the
"emoji-as-icons" pattern (the post-level count is inflated by emoji appearing in post
bodies generally, so weight the *icon* usage, not emoji in copy broadly).

**Quote:** "Emojis as icons. If I see them, I instantly doubt the creators ability to
even vibe-code properly. And it gives me a hint of the 'quality' of the backend." (r/ClaudeAI)

**Why it reads as AI:** 🚀 ✨ ⚡ 🔒 used as feature-card icons or section bullets is a
generated default (it needs no asset pipeline, so models reach for it). It renders
inconsistently across platforms and signals low effort.

**Code signatures (scanner):**
- Emoji inside heading/feature markup: emoji characters in `<h1>`/`<h2>`/`<h3>`, in
  feature-card titles, or as list bullets.
- The usual suspects as UI: 🚀 ✨ ⚡ 🔥 💡 🔒 ✅ 🎯 🌟 used as icons.

**Fix:** use a real icon set (Lucide, Phosphor, Heroicons rendered as SVG, or custom),
or no icon at all. Keep emoji out of headings and feature lists. Emoji in genuine body
copy where a person would actually use one is fine; emoji standing in for UI icons is not.

---

## 8. Generic sans fonts (Inter / Geist / Roboto / system default)

**Evidence:** named in 0.4% of comments by literal name, but the verifier flags this as
**understated**, because many more people just say "generic font" or "same fonts over and
over" without naming one.

**Quote:** Anthropic's own frontend-design skill, quoted by a user, says to avoid
"overused font families (Inter, Roboto, Arial, system fonts)." And: "Black background,
neon and lime green text. Inter font. 3 cards with thick border on left only" (offered as
a checklist of giveaways).

**Why it reads as AI:** Inter and Geist are the default sans for Tailwind/Next starters,
so leaving them is "nobody chose the type." Good type is one of the fastest ways to look
deliberate.

There are two default fonts now, not one. Inter/Geist is the "I didn't pick a font"
default. Instrument Serif/Fraunces is the "I tried to pick a tasteful font" default (see
tell 0). Both are autopilot. Reaching for either because it is what shows up first is the
tell, not the font itself.

**Code signatures (scanner):**
- Sans defaults: `font-family: Inter` / `Geist` / `Roboto` / `system-ui` as the only face.
- "Tasteful" serif defaults: `Instrument Serif`, `Fraunces`, `Playfair Display`,
  `Spectral`, `Cormorant`, `DM Serif Display` as the heading face.
- `next/font/google` importing one of the above with no real second face.
- Tailwind `font-sans` left at default with no custom font config.

**Fix:** choose a typeface with character for a reason, and pair it. A distinctive
display face over a clean body face breaks the default look, but only if the display
face is a real choice and not the current default-tasteful serif. The goal is a chosen
type system you can justify, not the starter font and not the starter "nice" font.

---

## 9. The centered hero + three feature cards + CTA skeleton

**Evidence:** named in 0.4% of comments and 1.6% of posts. Tied directly to shadcn
defaults by commenters.

**Quote:** "the issue usually isn't the prompt, it's that shadcn defaults give you
symmetric centered hero + 3 feature cards + cta, which is the dead giveaway. break the
grid: asymmetric hero, one oversized screenshot or loom-style video." (r/ClaudeCode)

**Why it reads as AI:** the exact page skeleton (centered hero with a big headline and two
buttons, then a 3-column grid of icon feature cards, then a centered CTA band) is the most
common generated landing page. The structure itself is the tell, before any color.

**Code signatures (scanner, heuristic):**
- A centered hero (`text-center` + big `text-5xl/6xl` headline + two buttons) immediately
  followed by `grid grid-cols-1 md:grid-cols-3` of cards with an icon + title + blurb.
- `gap-*` symmetric 3-up card grids repeated for "Features," "Benefits," "How it works."

**Fix:** break the grid. Use an asymmetric hero (content left, a real product screenshot
or short video right). Vary section layouts instead of stacking identical centered card
grids. Show the actual product over abstract icon-cards. Real screenshots beat three
icons-with-blurbs almost every time.

---

## 10. Layout-quality tells (overflow, spacing, alignment)

These are not color or font choices, so the scanner mostly cannot see them, but they are
a large part of why a generated page reads as AI. They were the most specific technical
complaints on the demo that prompted this rewrite ("text going behind the container,"
"heading needs an overflow," "inconsistent paddings, misaligned elements, no logic
behind the UI"). Check them by eye on every build.

- **Text overflow and clipping.** A heading or label that runs past or behind its
  container, or a fixed-width card that does not handle long content. Generated layouts
  often place absolutely-positioned text near a card and never test a real string. Give
  text room, let it wrap, and test with real content lengths.
- **Inconsistent spacing.** A page that mixes many unrelated padding and gap values
  (`p-3` here, `p-7` there, arbitrary `mt-[37px]`) has no spacing system, and the eye
  reads that as machine-made. Use one spacing scale and apply it consistently.
- **Misalignment.** Elements that almost line up but do not (off-by-a-few-pixels edges,
  inconsistent column gutters). Align to a grid.
- **No information hierarchy.** Every section the same weight, nothing leading the eye.
  Decide what the user should see first and make the layout say so.

Fixing color and font on an incoherent layout still leaves a site that reads as AI. The
scanner gives you a clean surface; these give you a coherent structure.

## 11. Lower-signal and copy tells

Real but minor; fix if cheap, do not over-rotate.

- **Centered everything / endless whitespace** (0.2% comments): huge vertical padding and
  everything centered. Vary alignment and tighten spacing.
- **Stock illustrations / clipart** (0.2%): undraw-style blobs and generic 3D. Use real
  screenshots or commissioned art.
- **Gradient hero copy clichés**: "Transform your X," "Supercharge," "Unleash,"
  "Effortlessly," "Your X, reimagined." These pair with the gradient-text tell. Write
  specific copy about what the thing actually does.
- **Glassmorphism** (0.2%) and **bento grids** (0.1%): low signal and contested. Allowed.

---

## 12. Cleared by the data (do not chase)

- **Mesh / blob / aurora backgrounds**: investigated and **rejected** as a keyword
  artifact (most matches were github "/blob/" URLs and metaphors). Not a real complaint.
- **Bento grids**: dead last at 0.1%, and people actively defend them. Not a tell.
- **Dark mode itself**: only the unprompted *glow* is flagged, not dark mode.
- **shadcn / Tailwind themselves**: the *defaults* are the tell, not the tools. A themed
  shadcn site is invisible to this complaint.

Flag what the data supports, at the weight it supports. Over-flagging makes the audit
noise.