Building a Color System: From One Seed to a Full Palette
Every design system starts with color. But choosing individual colors one by one leads to inconsistency and accessibility problems. A better approach is to start with a single seed color and systematically generate a full palette. This guide covers how that works.
Color theory fundamentals
Colors relate to each other through the color wheel. Understanding three basic relationships gives you a framework for building harmonious palettes.
Complementary colors
Two colors opposite each other on the wheel (e.g., blue and orange). They create maximum contrast and visual tension. Use complementary colors for CTAs that need to pop against a primary brand color.
Analogous colors
Three colors adjacent on the wheel (e.g., blue, blue-green, green). They create a harmonious, low-contrast palette. Good for backgrounds, cards, and subtle UI elements that should feel cohesive.
Triadic colors
Three colors equally spaced around the wheel (e.g., red, yellow, blue). They provide balanced variety. Useful for dashboards and data visualizations where you need distinct but non-clashing colors.
The HSL model: your best friend
RGB is how screens display color, but HSL (Hue, Saturation, Lightness) is how humans think about color. Building a palette in HSL is far more intuitive.
/* HSL is easier to reason about */
--primary-500: hsl(210, 80%, 50%); /* A vivid blue */
--primary-300: hsl(210, 80%, 70%); /* Lighter version: just increase L */
--primary-700: hsl(210, 80%, 30%); /* Darker version: just decrease L */
The key insight: to create a shade scale from a single color, you primarily adjust lightness while making subtle adjustments to saturation and sometimes hue to keep the color perceptually consistent.
Generating a Tailwind-style scale
Tailwind CSS uses a 50-950 scale for each color. The 500 shade is typically the "base" color, with lighter shades going down to 50 and darker shades up to 950.
Here is a practical algorithm for generating a 10-step scale from a seed color:
function generateScale(seedHue, seedSat) {
// Each step defines [lightness, saturation adjustment]
const steps = {
50: [97, -30],
100: [93, -20],
200: [85, -10],
300: [74, -5],
400: [62, 0],
500: [50, 0], // Base
600: [40, +5],
700: [32, +5],
800: [24, +5],
900: [18, +5],
950: [10, +5],
};
const scale = {};
for (const [step, [lightness, satAdj]] of Object.entries(steps)) {
const sat = Math.min(100, Math.max(0, seedSat + satAdj));
scale[step] = `hsl(${seedHue}, ${sat}%, ${lightness}%)`;
}
return scale;
}
const blue = generateScale(210, 80);
// blue[50] = "hsl(210, 50%, 97%)" -- near white with blue tint
// blue[500] = "hsl(210, 80%, 50%)" -- vivid blue
// blue[950] = "hsl(210, 85%, 10%)" -- near black with blue tint
The saturation adjustments are subtle but important. Very light shades look washed out if saturation stays high, and very dark shades look richer with slightly more saturation.
WCAG contrast requirements
A beautiful palette is useless if users cannot read the text. WCAG 2.1 defines two conformance levels for contrast ratios:
- AA (minimum): 4.5:1 for normal text, 3:1 for large text (18px bold or 24px regular)
- AAA (enhanced): 7:1 for normal text, 4.5:1 for large text
The contrast ratio formula compares the relative luminance of two colors on a scale from 1:1 (no contrast) to 21:1 (black on white).
function getContrastRatio(l1, l2) {
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
Practical pairing rules
For a typical color scale, these pairings tend to meet AA contrast:
| Background | Text color | Use case |
|---|---|---|
| 50 | 700, 800, 900 | Light mode body text |
| 100 | 800, 900 | Light mode secondary surfaces |
| 500 | white | Buttons, badges |
| 800 | 50, 100, 200 | Dark mode body text |
| 900 | 100, 200, 300 | Dark mode surfaces |
Always verify with an actual contrast checker. The exact ratios depend on your specific hue and saturation.
Building a complete design system palette
A production palette needs more than one color scale. Here is a minimal set:
:root {
/* Primary: your brand color */
--primary-50: hsl(210, 50%, 97%);
/* ... 100 through 900 ... */
--primary-500: hsl(210, 80%, 50%);
/* Neutral: for text, borders, backgrounds */
--neutral-50: hsl(210, 10%, 97%);
--neutral-500: hsl(210, 10%, 50%);
--neutral-900: hsl(210, 10%, 10%);
/* Semantic colors */
--success-500: hsl(145, 65%, 42%);
--warning-500: hsl(35, 90%, 55%);
--danger-500: hsl(0, 75%, 55%);
--info-500: hsl(200, 80%, 50%);
}
The neutral scale is often overlooked. Pure gray (hsl(0, 0%, X%)) looks lifeless. Adding a small amount of your primary hue's saturation (5-15%) to your neutrals creates a warmer, more cohesive feel.
Dark mode from the same palette
With a well-structured scale, dark mode is mostly about inverting which shades you use for backgrounds vs text:
/* Light mode */
.light {
--bg-primary: var(--neutral-50);
--text-primary: var(--neutral-900);
--surface: var(--neutral-100);
}
/* Dark mode: flip the scale */
.dark {
--bg-primary: var(--neutral-950);
--text-primary: var(--neutral-100);
--surface: var(--neutral-800);
}
Primary accent colors (buttons, links) often need adjustment in dark mode. The 500 shade that looks great on a light background may lack contrast on a dark one. Bump it to 400 or 300 for dark mode.
Common mistakes
Too many colors. If your palette has 15 distinct hues, it will look chaotic. Most apps need a primary, a neutral, and 3-4 semantic colors.
Ignoring perceived brightness. Yellow at 50% lightness looks much brighter than blue at 50% lightness. You may need to adjust lightness values per hue to achieve visual consistency across your palette.
Skipping the neutral scale. Using raw black (#000) and white (#fff) for text and backgrounds creates harsh contrast. A tinted neutral scale is easier on the eyes.
Not testing on real content. A palette that looks good on a color swatch page may fail on a dense data table or a long-form article. Always test with realistic layouts.
Summary
A systematic approach to color -- starting from a seed, generating scales through HSL manipulation, and validating against WCAG contrast -- produces a palette that is consistent, accessible, and easy to maintain. It beats picking colors by eye every time.
Try our Color Palette Generator to generate a full color system from any seed color instantly -- right in your browser, no upload required.