Building Color Palettes

A systematic approach to creating color palettes that work for real projects.

The Complete Palette Structure

A production-ready color palette needs:

┌─────────────────────────────────────────────────────────────┐
│                     COMPLETE PALETTE                        │
├─────────────────────────────────────────────────────────────┤
│  Neutrals (Gray scale)           │  50-950 (10+ shades)    │
│  Primary (Brand color)           │  50-950 (10+ shades)    │
│  Secondary (Optional)            │  50-950 (if needed)     │
│  Accent (Complementary)          │  50-950 (if needed)     │
├─────────────────────────────────────────────────────────────┤
│  Success (Green)                 │  light, base, dark      │
│  Warning (Yellow/Orange)         │  light, base, dark      │
│  Error (Red)                     │  light, base, dark      │
│  Info (Blue)                     │  light, base, dark      │
└─────────────────────────────────────────────────────────────┘

Step 1: Choose Your Base Colors

Selecting a Primary Color

Your primary color is usually determined by:

  • Existing brand guidelines
  • Industry conventions
  • Target audience
  • Emotional goals

If starting fresh, consider:

IndustryCommon Primary Hues
Tech/SaaSBlue (210-230°)
FinanceBlue (220°), Green (160°)
HealthGreen (150-170°), Blue (190-210°)
CreativePurple (270-290°), Pink (330-350°)
E-commerceOrange (20-40°), Blue (220°)
FoodRed (0-15°), Orange (25-40°)

Selecting an Accent Color

Use color harmony to find your accent:

:root {
  --primary-hue: 220;  /* Blue */

  /* Option 1: Complementary (high contrast) */
  --accent-hue: calc(var(--primary-hue) + 180);  /* 40° Orange */

  /* Option 2: Split-complementary (softer) */
  --accent-hue: calc(var(--primary-hue) + 150);  /* 10° Red-orange */

  /* Option 3: Analogous (harmonious, less contrast) */
  --accent-hue: calc(var(--primary-hue) + 60);   /* 280° Purple */
}

Step 2: Generate Color Scales

A color scale provides light-to-dark variations of each hue.

The 50-950 Scale

Standard naming convention (Tailwind-style):

ShadeUse CaseTypical Lightness
50Subtle backgrounds97%
100Hover backgrounds93%
200Borders, dividers85%
300Disabled text70%
400Placeholder text55%
500Base/main color45-50%
600Hover states38%
700Active states30%
800Headings22%
900Primary text15%
950Near black8%

Generating a Scale Manually (HSL Method)

:root {
  --primary-hue: 220;

  /* Light shades - decrease saturation as lightness increases */
  --primary-50:  hsl(var(--primary-hue), 30%, 97%);
  --primary-100: hsl(var(--primary-hue), 40%, 93%);
  --primary-200: hsl(var(--primary-hue), 50%, 85%);
  --primary-300: hsl(var(--primary-hue), 55%, 70%);
  --primary-400: hsl(var(--primary-hue), 60%, 58%);

  /* Base color */
  --primary-500: hsl(var(--primary-hue), 70%, 50%);

  /* Dark shades - increase saturation as lightness decreases */
  --primary-600: hsl(var(--primary-hue), 75%, 42%);
  --primary-700: hsl(var(--primary-hue), 80%, 34%);
  --primary-800: hsl(var(--primary-hue), 85%, 26%);
  --primary-900: hsl(var(--primary-hue), 90%, 18%);
  --primary-950: hsl(var(--primary-hue), 95%, 10%);
}

Key Insights for Good Scales

  1. Saturation varies with lightness:

    • Very light colors need lower saturation (or they look neon)
    • Very dark colors can handle higher saturation
  2. Hue can shift slightly:

    • Warm colors often shift toward orange as they lighten
    • Cool colors often shift toward blue as they darken
    • This mimics how pigments work in real life
  3. Test every shade:

    • Light shades: backgrounds, hover states
    • Mid shades: borders, icons
    • Dark shades: text, active states

Step 3: Create Neutral Scale

Neutrals are the foundation. Tint them with your primary hue for cohesion.

Pure Gray vs Tinted Gray

/* Pure gray (can feel cold or disconnected) */
--gray-500: hsl(0, 0%, 50%);

/* Warm tinted gray */
--gray-500: hsl(40, 5%, 50%);

/* Cool tinted gray (blue) - matches blue primary */
--gray-500: hsl(220, 10%, 50%);

/* Brand-tinted gray (use your primary hue) */
--gray-500: hsl(var(--primary-hue), 8%, 50%);

Complete Neutral Scale

:root {
  --neutral-hue: 220;  /* Match primary for cohesion */

  --gray-50:  hsl(var(--neutral-hue), 15%, 98%);
  --gray-100: hsl(var(--neutral-hue), 12%, 95%);
  --gray-200: hsl(var(--neutral-hue), 10%, 88%);
  --gray-300: hsl(var(--neutral-hue), 8%, 75%);
  --gray-400: hsl(var(--neutral-hue), 6%, 58%);
  --gray-500: hsl(var(--neutral-hue), 6%, 45%);
  --gray-600: hsl(var(--neutral-hue), 8%, 35%);
  --gray-700: hsl(var(--neutral-hue), 10%, 25%);
  --gray-800: hsl(var(--neutral-hue), 12%, 17%);
  --gray-900: hsl(var(--neutral-hue), 15%, 10%);
  --gray-950: hsl(var(--neutral-hue), 18%, 5%);
}

Step 4: Define Semantic Colors

Success (Green)

:root {
  --success-hue: 142;

  --success-50:  hsl(var(--success-hue), 70%, 95%);
  --success-100: hsl(var(--success-hue), 65%, 88%);
  --success-200: hsl(var(--success-hue), 60%, 75%);
  --success-300: hsl(var(--success-hue), 55%, 60%);
  --success-400: hsl(var(--success-hue), 60%, 48%);
  --success-500: hsl(var(--success-hue), 70%, 40%);  /* Main */
  --success-600: hsl(var(--success-hue), 75%, 33%);
  --success-700: hsl(var(--success-hue), 80%, 26%);
  --success-800: hsl(var(--success-hue), 85%, 20%);
  --success-900: hsl(var(--success-hue), 90%, 14%);
}

Warning (Yellow/Amber)

:root {
  --warning-hue: 38;

  --warning-50:  hsl(var(--warning-hue), 95%, 95%);
  --warning-100: hsl(var(--warning-hue), 90%, 85%);
  --warning-200: hsl(var(--warning-hue), 85%, 72%);
  --warning-300: hsl(var(--warning-hue), 85%, 58%);
  --warning-400: hsl(var(--warning-hue), 90%, 48%);
  --warning-500: hsl(var(--warning-hue), 95%, 42%);  /* Main */
  --warning-600: hsl(var(--warning-hue), 95%, 35%);
  --warning-700: hsl(var(--warning-hue), 90%, 28%);
  --warning-800: hsl(var(--warning-hue), 85%, 22%);
  --warning-900: hsl(var(--warning-hue), 80%, 16%);
}

Error (Red)

:root {
  --error-hue: 0;

  --error-50:  hsl(var(--error-hue), 85%, 97%);
  --error-100: hsl(var(--error-hue), 80%, 92%);
  --error-200: hsl(var(--error-hue), 75%, 82%);
  --error-300: hsl(var(--error-hue), 70%, 68%);
  --error-400: hsl(var(--error-hue), 72%, 55%);
  --error-500: hsl(var(--error-hue), 75%, 48%);  /* Main */
  --error-600: hsl(var(--error-hue), 78%, 40%);
  --error-700: hsl(var(--error-hue), 80%, 32%);
  --error-800: hsl(var(--error-hue), 82%, 25%);
  --error-900: hsl(var(--error-hue), 85%, 18%);
}

Step 5: Verify Accessibility

Check contrast ratios for critical combinations:

CombinationMinimumTarget
gray-900 on white4.5:112:1+ ✓
gray-600 on white4.5:15:1+ ✓
primary-500 on white4.5:1Test!
white on primary-5004.5:1Test!
white on primary-6004.5:1Usually passes
success-500 on white4.5:1Test!
error-500 on white4.5:1Test!

Adjust shades if they don't pass.

Step 6: Create Semantic Tokens

Map your scales to meaningful names:

:root {
  /* Backgrounds */
  --bg-primary: white;
  --bg-secondary: var(--gray-50);
  --bg-tertiary: var(--gray-100);
  --bg-inverse: var(--gray-900);

  /* Text */
  --text-primary: var(--gray-900);
  --text-secondary: var(--gray-600);
  --text-muted: var(--gray-500);
  --text-inverse: white;

  /* Borders */
  --border-light: var(--gray-200);
  --border-default: var(--gray-300);
  --border-dark: var(--gray-400);

  /* Interactive */
  --interactive-default: var(--primary-500);
  --interactive-hover: var(--primary-600);
  --interactive-active: var(--primary-700);

  /* States */
  --state-success-bg: var(--success-50);
  --state-success-border: var(--success-200);
  --state-success-text: var(--success-700);

  --state-error-bg: var(--error-50);
  --state-error-border: var(--error-200);
  --state-error-text: var(--error-700);
}

Complete Palette Example

Here's a complete, production-ready palette:

:root {
  /* ========================================
     NEUTRALS (Blue-tinted)
     ======================================== */
  --gray-50:  #f8fafc;
  --gray-100: #f1f5f9;
  --gray-200: #e2e8f0;
  --gray-300: #cbd5e1;
  --gray-400: #94a3b8;
  --gray-500: #64748b;
  --gray-600: #475569;
  --gray-700: #334155;
  --gray-800: #1e293b;
  --gray-900: #0f172a;
  --gray-950: #020617;

  /* ========================================
     PRIMARY (Blue)
     ======================================== */
  --primary-50:  #eff6ff;
  --primary-100: #dbeafe;
  --primary-200: #bfdbfe;
  --primary-300: #93c5fd;
  --primary-400: #60a5fa;
  --primary-500: #3b82f6;
  --primary-600: #2563eb;
  --primary-700: #1d4ed8;
  --primary-800: #1e40af;
  --primary-900: #1e3a8a;
  --primary-950: #172554;

  /* ========================================
     SUCCESS (Green)
     ======================================== */
  --success-50:  #f0fdf4;
  --success-100: #dcfce7;
  --success-200: #bbf7d0;
  --success-300: #86efac;
  --success-400: #4ade80;
  --success-500: #22c55e;
  --success-600: #16a34a;
  --success-700: #15803d;
  --success-800: #166534;
  --success-900: #14532d;

  /* ========================================
     WARNING (Amber)
     ======================================== */
  --warning-50:  #fffbeb;
  --warning-100: #fef3c7;
  --warning-200: #fde68a;
  --warning-300: #fcd34d;
  --warning-400: #fbbf24;
  --warning-500: #f59e0b;
  --warning-600: #d97706;
  --warning-700: #b45309;
  --warning-800: #92400e;
  --warning-900: #78350f;

  /* ========================================
     ERROR (Red)
     ======================================== */
  --error-50:  #fef2f2;
  --error-100: #fee2e2;
  --error-200: #fecaca;
  --error-300: #fca5a5;
  --error-400: #f87171;
  --error-500: #ef4444;
  --error-600: #dc2626;
  --error-700: #b91c1c;
  --error-800: #991b1b;
  --error-900: #7f1d1d;

  /* ========================================
     SEMANTIC TOKENS
     ======================================== */

  /* Backgrounds */
  --bg-page: var(--gray-50);
  --bg-surface: white;
  --bg-elevated: white;
  --bg-muted: var(--gray-100);

  /* Text */
  --text-primary: var(--gray-900);
  --text-secondary: var(--gray-600);
  --text-muted: var(--gray-500);
  --text-disabled: var(--gray-400);
  --text-link: var(--primary-600);

  /* Borders */
  --border-subtle: var(--gray-200);
  --border-default: var(--gray-300);
  --border-strong: var(--gray-400);

  /* Focus */
  --focus-ring: var(--primary-500);
}

Tools for Generating Palettes

Automated Generators

  • Tailwind CSS Colors: Use as starting point
  • Radix Colors: Accessible color scales
  • Open Color: Open source palette
  • Coolors.co: Generate harmonious palettes
  • Realtime Colors: Preview palette on actual UI

In-Browser Tools

  • Chrome DevTools: Color picker with contrast checker
  • Figma: Built-in color management

Summary

StepAction
1Choose primary hue based on brand/industry
2Select accent using color harmony (complement, split)
3Generate 50-950 scales for each hue
4Create tinted neutral scale
5Add semantic colors (success, warning, error)
6Verify contrast ratios
7Create semantic tokens
8Document and maintain

Next: 09-dark-mode.md - Designing for dark themes