Skip to main content
Visual Arts & DesignWeb Polish185 lines

Color System Repair

Fix color chaos in a vibecoded website — too many near-duplicate colors, inconsistent semantic usage, broken dark mode, contrast failures, and colors without purpose. Establish a minimal palette with clear roles. Trigger when colors "don't match," there are too many shades of gray, or the palette feels random.

Quick Summary34 lines
Color chaos in vibecoded websites is an accumulation problem, not a design problem. Each AI prompt generates locally reasonable color values, but accumulated across dozens of prompts, the site ends up with thirty "slightly different grays" that make everything feel subtly wrong. The fix is not to redesign the palette but to consolidate: choose one neutral scale, assign semantic roles to every color, and mechanically replace every hardcoded value with a token reference.

## Key Points

- **Near-duplicate values.** `#333333`, `#2d2d2d`, `#374151`, `#3f3f46` all used as "dark text"
- **No semantic mapping.** The same blue is used for links, primary buttons, info alerts, and
- **Orphan colors.** A green that appears exactly once on a success badge. A specific purple
- **Contrast failures.** Light gray text on white backgrounds. Low-contrast placeholder text.
- **Partial dark mode.** Some components respect dark mode, others have hardcoded white backgrounds.
- Don't use opacity to create lighter shades. `bg-blue-500/20` looks different from `bg-blue-100`
- Don't mix Tailwind color families (slate + gray + zinc in the same component). Pick one.
- Don't use `text-gray-300` for text on white backgrounds. It fails contrast.
- Don't hardcode dark mode colors. Use CSS variables that remap in dark context.
- Don't create custom colors for one-off elements. Use the palette you have.
- Don't use more than one accent color unless the brand requires it. One primary + neutral scale

## Quick Example

```
Slate  (blue undertone)  — Cool, techy, modern
Gray   (true neutral)    — Safe, works everywhere
Zinc   (slightly warm)   — Balanced, slightly sophisticated
Neutral (pure neutral)   — Clean, no bias
Stone  (warm undertone)  — Earthy, editorial, warm
```

```
#f9fafb, #fafafa, #f3f4f6, #f4f4f5 → var(--color-bg-secondary)
#333, #374151, #27272a, #1f2937    → var(--color-text-primary)
#6b7280, #71717a, #52525b          → var(--color-text-secondary)
#e5e7eb, #e4e4e7, #d4d4d8          → var(--color-border-default)
```
skilldb get web-polish-skills/color-system-repairFull skill: 185 lines
Paste into your CLAUDE.md or agent config

Color System Repair

Core Philosophy

Color chaos in vibecoded websites is an accumulation problem, not a design problem. Each AI prompt generates locally reasonable color values, but accumulated across dozens of prompts, the site ends up with thirty "slightly different grays" that make everything feel subtly wrong. The fix is not to redesign the palette but to consolidate: choose one neutral scale, assign semantic roles to every color, and mechanically replace every hardcoded value with a token reference.

Every color in a healthy system must have a job. Background colors, text colors, border colors, interactive colors, and status colors each serve a distinct purpose. When the same blue is used for links, primary buttons, info alerts, and decorative backgrounds, changing any one of them requires changing all of them. Semantic color roles decouple visual appearance from usage, making the system maintainable and adaptable.

Contrast is accessibility, and accessibility is not optional. Every text-background combination must pass WCAG AA standards: 4.5:1 for normal text, 3:1 for large text. This is not a design preference -- it is a requirement that determines whether people with low vision can use your site. Checking contrast is a mechanical verification that should be part of every color system repair.

You are a color specialist who fixes the visual incoherence caused by vibecoded color decisions. Every AI prompt generates its own color values — #f1f5f9 here, #f3f4f6 there, #fafafa somewhere else — and the accumulated result is a site with 30 "slightly different grays" that make everything feel subtly wrong.

The Diagnosis

What Bad Color Looks Like

  • Near-duplicate values. #333333, #2d2d2d, #374151, #3f3f46 all used as "dark text" across different components. The differences are invisible but the inconsistency is real.
  • No semantic mapping. The same blue is used for links, primary buttons, info alerts, and decorative backgrounds. When one needs to change, everything changes.
  • Orphan colors. A green that appears exactly once on a success badge. A specific purple used in one header. Colors with no family or purpose.
  • Contrast failures. Light gray text on white backgrounds. Low-contrast placeholder text. Colored buttons with unreadable text.
  • Partial dark mode. Some components respect dark mode, others have hardcoded white backgrounds.

The Audit

# Extract every color value
grep -roh '#[0-9a-fA-F]\{3,8\}' src/ --include="*.tsx" --include="*.css" --include="*.scss" | sort | uniq -c | sort -rn

# Extract Tailwind color classes
grep -roh '\(bg\|text\|border\|ring\|shadow\)-\(gray\|slate\|zinc\|neutral\|stone\|red\|orange\|amber\|yellow\|lime\|green\|emerald\|teal\|cyan\|sky\|blue\|indigo\|violet\|purple\|fuchsia\|pink\|rose\)-[0-9]*' src/ | sort | uniq -c | sort -rn

# Find hardcoded colors (not using variables/tokens)
grep -rn 'color:\s*#' src/ --include="*.css" --include="*.scss"
grep -rn 'style.*color.*#' src/ --include="*.tsx" --include="*.jsx"

The Fix: A Minimal, Purposeful Palette

Step 1: Choose Your Neutral Scale

Every site needs exactly ONE neutral scale. Pick based on undertone:

Slate  (blue undertone)  — Cool, techy, modern
Gray   (true neutral)    — Safe, works everywhere
Zinc   (slightly warm)   — Balanced, slightly sophisticated
Neutral (pure neutral)   — Clean, no bias
Stone  (warm undertone)  — Earthy, editorial, warm

Consolidate ALL grays/near-blacks/near-whites to your chosen scale:

/* BEFORE: 5 different "grays" from different Tailwind palettes */
.card      { background: #f1f5f9; }  /* slate-100 */
.sidebar   { background: #f3f4f6; }  /* gray-100 */
.panel     { background: #fafafa; }  /* neutral-50 */
.dropdown  { background: #f4f4f5; }  /* zinc-100 */
.tooltip   { background: #f5f5f4; }  /* stone-100 */

/* AFTER: one scale, one "light surface" token */
.card, .sidebar, .panel, .dropdown, .tooltip {
  background: var(--color-bg-secondary); /* maps to zinc-100: #f4f4f5 */
}

Step 2: Define Color Roles

Every color must have a job. If it doesn't have a role, it doesn't belong:

:root {
  /* === BACKGROUNDS === */
  --color-bg-primary:    #ffffff;           /* Main content area */
  --color-bg-secondary:  var(--zinc-50);    /* Cards, sidebars, sections */
  --color-bg-tertiary:   var(--zinc-100);   /* Inset areas, table headers */
  --color-bg-inverse:    var(--zinc-900);   /* Dark sections, tooltips */

  /* === TEXT === */
  --color-text-primary:    var(--zinc-900);   /* Headings, primary content */
  --color-text-secondary:  var(--zinc-600);   /* Supporting text, descriptions */
  --color-text-tertiary:   var(--zinc-400);   /* Placeholders, disabled text */
  --color-text-inverse:    #ffffff;            /* Text on dark backgrounds */

  /* === BORDERS === */
  --color-border-default:  var(--zinc-200);   /* Cards, inputs, dividers */
  --color-border-strong:   var(--zinc-300);   /* Emphasized borders */
  --color-border-subtle:   var(--zinc-100);   /* Barely-visible dividers */

  /* === INTERACTIVE === */
  --color-primary-50:  #eff6ff;     /* Primary tinted background */
  --color-primary-100: #dbeafe;     /* Primary hover background */
  --color-primary-500: #3b82f6;     /* Primary default */
  --color-primary-600: #2563eb;     /* Primary hover */
  --color-primary-700: #1d4ed8;     /* Primary active */

  /* === STATUS === */
  --color-success-bg:     #f0fdf4;  --color-success:     #22c55e;  --color-success-text:  #166534;
  --color-warning-bg:     #fffbeb;  --color-warning:     #f59e0b;  --color-warning-text:  #92400e;
  --color-error-bg:       #fef2f2;  --color-error:       #ef4444;  --color-error-text:    #991b1b;
  --color-info-bg:        #eff6ff;  --color-info:        #3b82f6;  --color-info-text:     #1e40af;
}

Step 3: Dark Mode (If Applicable)

Dark mode means remapping semantic tokens, not inverting colors:

[data-theme="dark"], .dark {
  --color-bg-primary:    var(--zinc-950);
  --color-bg-secondary:  var(--zinc-900);
  --color-bg-tertiary:   var(--zinc-800);
  --color-bg-inverse:    var(--zinc-100);

  --color-text-primary:    var(--zinc-50);
  --color-text-secondary:  var(--zinc-400);
  --color-text-tertiary:   var(--zinc-500);
  --color-text-inverse:    var(--zinc-900);

  --color-border-default:  var(--zinc-800);
  --color-border-strong:   var(--zinc-700);
  --color-border-subtle:   var(--zinc-800);

  /* Primary gets slightly lighter for dark backgrounds */
  --color-primary-500: #60a5fa;
  --color-primary-600: #3b82f6;
}

Step 4: Contrast Verification

Check every text/background combination:

PASS: zinc-900 on white      = 17.4:1  (AAA)
PASS: zinc-600 on white      = 5.7:1   (AA)
PASS: zinc-400 on white      = 3.5:1   (AA Large only — only for large text!)
FAIL: zinc-300 on white      = 2.6:1   (FAIL — never use for text)

PASS: white on primary-600   = 7.8:1   (AAA)
PASS: white on primary-500   = 4.6:1   (AA)
FAIL: white on primary-400   = 3.2:1   (FAIL — button text won't be readable)

Tools: Use browser DevTools color picker, or a contrast checker extension. Every text element must pass WCAG AA (4.5:1 for normal text, 3:1 for large text).

Step 5: Mechanical Replacement

Replace hardcoded colors file by file:

#f9fafb, #fafafa, #f3f4f6, #f4f4f5 → var(--color-bg-secondary)
#333, #374151, #27272a, #1f2937    → var(--color-text-primary)
#6b7280, #71717a, #52525b          → var(--color-text-secondary)
#e5e7eb, #e4e4e7, #d4d4d8          → var(--color-border-default)

Anti-Patterns

  • Don't use opacity to create lighter shades. bg-blue-500/20 looks different from bg-blue-100 and breaks when layered on non-white backgrounds.
  • Don't mix Tailwind color families (slate + gray + zinc in the same component). Pick one.
  • Don't use text-gray-300 for text on white backgrounds. It fails contrast.
  • Don't hardcode dark mode colors. Use CSS variables that remap in dark context.
  • Don't create custom colors for one-off elements. Use the palette you have.
  • Don't use more than one accent color unless the brand requires it. One primary + neutral scale handles 95% of UI needs.

Install this skill directly: skilldb add web-polish-skills

Get CLI access →

Related Skills

Component Unification

Take scattered, inconsistent UI components from a vibecoded website and unify them into a shared component library where every button, input, card, badge, and modal uses the same design language. Trigger when users want to "make components match," "create a component library," "unify the UI," or "build shared components."

Web Polish228L

Dark Mode Retrofit

Add consistent dark mode to an existing website or fix partial/broken dark mode implementation. Covers CSS variable theming, component-by-component migration, image handling, and testing. Trigger when dark mode "doesn't work on some pages," "looks broken," or needs to be added from scratch.

Web Polish296L

Design Token Extraction

Extract a unified set of design tokens (colors, typography, spacing, shadows, radii) from a messy vibecoded codebase and establish a single source of truth. Trigger when users want to "unify the styles," "create CSS variables," "make a theme," "extract tokens," or "set up a design system foundation."

Web Polish289L

Form Elements

Unify all form elements — inputs, selects, textareas, checkboxes, toggles, radio buttons, file uploads, date pickers, and form layouts — into a consistent, accessible system. Trigger when forms look different across pages, upload buttons don't match, or form validation styling is inconsistent.

Web Polish408L

Modal Dialog System

Unify all modals, dialogs, drawers, sheets, and overlay patterns across a website into a single consistent system. Trigger when modals look different across pages, close buttons don't match, overlays have different opacity, or dialog behavior is inconsistent.

Web Polish317L

Navigation Patterns

Fix inconsistent navigation patterns — mismatched headers, footers, sidebars, breadcrumbs, tab bars, and mobile menus. Establish unified navigation components that work across all pages. Trigger when nav "looks different on different pages," active states don't match, or mobile menu is broken/missing.

Web Polish318L