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:
| Industry | Common Primary Hues |
|---|---|
| Tech/SaaS | Blue (210-230°) |
| Finance | Blue (220°), Green (160°) |
| Health | Green (150-170°), Blue (190-210°) |
| Creative | Purple (270-290°), Pink (330-350°) |
| E-commerce | Orange (20-40°), Blue (220°) |
| Food | Red (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):
| Shade | Use Case | Typical Lightness |
|---|---|---|
| 50 | Subtle backgrounds | 97% |
| 100 | Hover backgrounds | 93% |
| 200 | Borders, dividers | 85% |
| 300 | Disabled text | 70% |
| 400 | Placeholder text | 55% |
| 500 | Base/main color | 45-50% |
| 600 | Hover states | 38% |
| 700 | Active states | 30% |
| 800 | Headings | 22% |
| 900 | Primary text | 15% |
| 950 | Near black | 8% |
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
Saturation varies with lightness:
- Very light colors need lower saturation (or they look neon)
- Very dark colors can handle higher saturation
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
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:
| Combination | Minimum | Target |
|---|---|---|
| gray-900 on white | 4.5:1 | 12:1+ ✓ |
| gray-600 on white | 4.5:1 | 5:1+ ✓ |
| primary-500 on white | 4.5:1 | Test! |
| white on primary-500 | 4.5:1 | Test! |
| white on primary-600 | 4.5:1 | Usually passes |
| success-500 on white | 4.5:1 | Test! |
| error-500 on white | 4.5:1 | Test! |
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
| Step | Action |
|---|---|
| 1 | Choose primary hue based on brand/industry |
| 2 | Select accent using color harmony (complement, split) |
| 3 | Generate 50-950 scales for each hue |
| 4 | Create tinted neutral scale |
| 5 | Add semantic colors (success, warning, error) |
| 6 | Verify contrast ratios |
| 7 | Create semantic tokens |
| 8 | Document and maintain |
Next: 09-dark-mode.md - Designing for dark themes