DEV Community

Cover image for Your vibe-coded app looks ugly. Here's the one-file fix.
@kiwibreaksme
@kiwibreaksme

Posted on

Your vibe-coded app looks ugly. Here's the one-file fix.

StyleSeed live demo

Let's be honest about vibe coding

You tell Claude Code "build me a SaaS dashboard." Thirty seconds later you have a working app. Revenue chart, user table, activity feed. Everything functional.

And it looks like every other AI-generated app on the internet.

Spacing that doesn't breathe. Seven accent colors fighting for attention. Cards that float on the page with zero separation from the background. The components are all shadcn — the technical stack is fine. But the result is amateur.

I spent a week trying to fix this through prompts. "Use only one accent color." "Card background #FFF, page background #FAFAFA." "Smaller shadows." Every fix worked for one screen, then the next screen forgot.

Then it clicked: the model doesn't need better prompts. It needs a design brain.

Pro designers aren't using better components — they're using invisible rules

Ask a senior designer why their dashboard looks refined and they'll say things like:

"Never use pure black. Use #2A2A2A."

"One accent color per app. Everything else grayscale."

"Shadows at 4% opacity. If you can see it, it's already too much."

Nobody writes these down. They're baked into years of experience, invisible to outsiders — which means invisible to LLMs. No matter how many shadcn components Claude has in its training data, it has never been told when to use which.

So I sat down and extracted 69 of these rules from Toss, Stripe, Linear, and Vercel — the brands whose output I wanted Claude to imitate — and wrote them into a single DESIGN-LANGUAGE.md file that Claude reads automatically.

The 69 rules, organized

The rules cluster into six groups:

  • Color discipline (12 rules) — #2A2A2A as the refined black, 5-level grayscale (#2A → #3C → #6A → #7A → #9B), one accent color maximum, no pure white on pure white.
  • Spatial rhythm (14 rules) — never repeat the same section type consecutively, alternate tall/compact, 2:1 number-to-unit ratios (48px value, 24px unit).
  • Information hierarchy (9 rules) — card/background separation (#FFF card on #FAFAFA page) matters more than any border, density increases as you scroll down, top is big numbers, bottom is dense lists.
  • Shadow & elevation (8 rules) — max 4% opacity, elevation is never z-index, dark mode replaces shadows with borders.
  • Component variance (11 rules) — never 4 identical KPI cards, stagger content types (2 with trend, 1 with progress, 1 with comparison), avoid "filled form of sameness."
  • Motion & feedback (15 rules) — 200ms normal, spring for entrance, ease-out for exit, hover lifts by 6px max, tap scales to 0.98.

Drop the file into a project, Claude reads it, Cursor reads it (via .cursorrules). Same prompt, same model — the output quality jumps.

One rule, unpacked

Take the #2A2A2A rule. Pure #000 on #FFF has a contrast ratio of 21:1 — the highest possible. Sounds like a win. But at that contrast the eye gets fatigued fast, and the text starts to feel harsh and heavy on a white page.

#2A2A2A drops the contrast to about 15:1. Still WCAG AAA for body text. But the page stops "vibrating." Every refined design system in the last decade — Apple, Linear, Vercel, Notion, Toss — uses a softened near-black for body text. Claude picks #000 by default because it's the highest-contrast option, which is technically correct and aesthetically wrong.

One rule. Fixes thousands of screens.

Now multiply that by 69.

Same component, three brand DNAs

The live demo up top is a real app — styleseed-demo.vercel.app. Click the Toss / Raycast / Arc switcher. Same chat component, three totally different brand identities — colors, radius, shadows, motion durations, gradients, even the phone frame's ambient light.

The switcher flips a single data-skin attribute on the wrapper. That's it. No conditional rendering, no theme provider, no re-mount.

How it actually works

Every visual value is a CSS variable scoped to [data-skin="..."]:

[data-skin="toss"] {
  --brand: #3182F6;
  --radius: 0.875rem;
  --shadow-card: 0 1px 3px rgba(0,0,0,0.04);
  --gradient-brand: linear-gradient(135deg, #3182F6, #4A90F7);
  --duration-normal: 200ms;
}

[data-skin="raycast"] {
  --brand: #FF4E8B;
  --radius: 0.5rem;
  --shadow-card: 0 0 0 1px rgba(255,255,255,0.05),
                 0 8px 24px rgba(0,0,0,0.4);
  --gradient-brand: linear-gradient(135deg, #FF6363, #FF8E3C 30%,
                    #E84A8E 65%, #A855F7);
  --duration-normal: 220ms;
}
Enter fullscreen mode Exit fullscreen mode

Components never touch the raw values — only the tokens:

<motion.div
  style={{
    background: "var(--card)",
    borderRadius: "var(--radius-xl)",
    boxShadow: "var(--shadow-card)",
  }}
/>
Enter fullscreen mode Exit fullscreen mode

Swap the data attribute → the entire tree morphs in one frame. Framer Motion animates the transitions for free because the underlying CSS properties are inherited.

This is the trick the 69 rules depend on. Rules reference semantic tokens (--brand, --card, --shadow-card), not literal values. So the same rulebook works whether your app looks like Toss or Vercel or your client's weird purple brand.

Why this matters for vibe coders

The point of vibe coding is shipping fast. The problem with vibe coding is the output screams "I was made in 30 minutes."

StyleSeed closes that gap in two moves:

  1. The 69 rules fix the judgment. Claude stops picking #000 for text and py-4 for everything. The output starts looking designed, not generated.
  2. The token structure fixes the brand. Your MVP can ship with Toss skin today and re-skin to a client's brand tomorrow by swapping one file. Same codebase. Zero rewrites.

Both drop in with one command. Engine is brand-agnostic — the rules don't know what color your brand is.

Stack

  • Next.js 16 (App Router, Turbopack)
  • Tailwind CSS v4 (the @theme inline syntax makes token sharing trivial)
  • Framer Motion for the morph animation
  • Radix under the components
  • Free, MIT

Try it

If you're vibe coding anything right now, drop the engine in and compare the output. Honest critique on the rules themselves very welcome — which one feels wrong? Which one's missing? The components are downstream. The rules are the actual product.

Top comments (0)