Web DesignJuly 28, 2026 · 10 min read

How to Build a Design System Color Palette From a Single Brand Color

Turn one brand hex code into a full design system palette: tint and shade ramps, neutral scales, semantic colors, dark mode, and export-ready tokens — step by step.

The brief arrives with confidence and one line: "Our brand color is #2563EB. Make it work." No neutrals, no secondary palette, no guidance for states, charts, or dark mode. This scenario — expanding a single color into a complete system — is one of the most common tasks in product design, and it's far more mechanical than most people expect.

Here's the full method, used by virtually every major design system (Material, Radix, Tailwind), adapted so you can execute it in an afternoon.

Step 1: Interrogate the brand color

Before generating anything, convert your seed into HSL to understand what you're working with. Take #2563EBhsl(221°, 83%, 53%).

Three properties determine everything downstream:

  • Hue (221°): a confident blue — cool family, plays well with slate/gray neutrals.
  • Saturation (83%): vivid but not neon; usable as-is for buttons at mid-lightness.
  • Lightness (53%): conveniently close to the sweet spot where white text still passes WCAG contrast against it (verify — don't assume).

If your seed sits at extreme lightness (very pale or very dark), note that it will serve better as a surface or text color than a button color, and plan your ramp accordingly. You can inspect any seed across all models instantly with our image picker or read up on why HSL is the right working model in our color models comparison.

Step 2: Build the primary ramp

A ramp (or scale) is 9–11 steps of the same hue from near-white to near-black. Number them like Tailwind does:

| Step | Role | Target lightness | | --- | --- | --- | | 50 | Subtlest background tint | ~97% | | 100 | Light background / hover of 50 | ~94% | | 200 | Borders on light surfaces | ~87% | | 300 | Muted accents, disabled | ~76% | | 400 | Secondary interactive | ~64% | | 500 | Primary brand anchor | ~50% | | 600 | Hover state of primary | ~44% | | 700 | Active/pressed state | ~37% | | 800 | Text on light backgrounds | ~30% | | 900 | Deep text / headings | ~22% | | 950 | Near-black accent | ~14% |

The mechanics: keep hue constant (±5° drift is fine and often adds sophistication — slightly cooler toward dark, warmer toward light), reduce saturation slightly at the extremes (full saturation looks toxic at very light steps and muddy at very dark ones), and move lightness in even perceptual increments rather than linear ones — steps look even when each jump is roughly 6–8 points of perceived lightness near the middle.

Do this by hand in HSL, or shortcut it: lock your seed color in the palette generator and use harmony modes to explore variations, then fine-tune numerically.

Step 3: Construct a neutral scale that belongs to the brand

Pure gray (#808080 everywhere) is the tell of an amateur system. Professional systems tint their neutrals with the brand hue at low saturation — typically 5–15%. For our blue seed:

  • Neutral 0: #F8FAFC
  • Neutral 100: #F1F5F9
  • Neutral 200: #E2E8F0
  • Neutral 500: #64748B (secondary text)
  • Neutral 900: #0F172A

This is Slate — blue-gray, hue ~215°. The brand and its neutrals now whisper the same temperature. The test: put your neutral-100 surface next to your primary-100 tint; they should feel like relatives, not strangers.

Reserve neutral-900 (not pure black) for body text and neutral-500 for secondary text, then verify both hit 4.5:1 on your lightest surfaces — they will if constructed this way, which is exactly the point.

Step 4: Add semantic colors

Semantics answer "what happened?" independently of brand identity:

  • Success: green, e.g., ramp anchored at hsl(142°, 71%, 45%)
  • Warning: amber, anchored around hsl(38°, 92%, 50%)
  • Danger/error: red — even if the brand is red. If collision is unavoidable, push danger toward crimson (hsl(0°, 72%, 51%)) and brand away from it.
  • Info: usually the brand itself or a cyan sibling.

Each semantic needs its own mini-ramp with the same structure: a pale tint for alert backgrounds, a mid tone for borders/icons, and a deep tone for text — plus verified contrast for any text-on-color usage. Three steps per semantic is usually enough; five if your product has dense status UI.

Step 5: Define interaction states

Map every interactive component to specific ramp steps so states are consistent app-wide:

Primary button:
  default     → primary-600
  hover       → primary-700
  active      → primary-800
  disabled    → primary-200 (or neutral-200)
  focus ring  → primary-400 @ 40% opacity

Links:        → primary-700 (underlined)
Selection:    → primary-100

Note the pattern: buttons live one step darker than you'd guess (600–700 rather than 500), because mid-ranch 500 is reserved for large surfaces where its vibrancy reads best without carrying text contrast obligations.

Step 6: Dark mode without reinvention

A proper ramp makes dark mode mostly mechanical:

  1. Invert surface logic: neutral-950 becomes the canvas, neutral-900 cards, neutral-100 borders.
  2. Re-anchor the primary ramp upward: what was primary-500 on light becomes primary-300/400 on dark, because saturated mid-tones vibrate against black and fail contrast.
  3. Desaturate slightly across the board — colors appear more intense against dark surfaces.
  4. Re-verify every text pair. Contrast ratios must be recomputed, not assumed; luminance relationships flip unpredictably.

If your tokens are named semantically (bg-surface, text-primary, border-subtle) rather than literally (gray-100), dark mode becomes a token remap instead of a redesign.

Step 7: Name and export tokens

Final structure worth adopting regardless of stack:

{
  "color": {
    "brand": { "50": "...", "500": "#2563EB", "900": "..." },
    "neutral": { "0": "...", "900": "..." },
    "semantic": {
      "success": { "bg": "...", "fg": "...", "border": "..." },
      "danger":  { "bg": "...", "fg": "...", "border": "..." }
    },
    "surface":  { "default": "neutral-0", "inverse": "neutral-950" },
    "text":     { "primary": "neutral-900", "secondary": "neutral-500" },
    "action":   { "primary": "brand-600", "primary-hover": "brand-700" }
  }
}

The two-tier naming (literal ramps + semantic roles) is the industry-standard compromise: designers reference roles, engineers can retheme by editing mappings.

When the structure is set, generate production output directly: our palette exporter converts palettes into Tailwind config, CSS custom properties, JSON tokens, and SVG spec sheets — the formats engineering teams actually consume.

Common failure modes

  • Skipping the neutral tint and pairing brand blue with pure grays — the system never coheres.
  • One ramp for everything — using primary-500 for buttons, links, badges, and charts until nothing stands out.
  • Contrast checked only in light mode, then dark mode ships failing pairs.
  • Semantic colors matching the brand (blue brand + blue info + blue links = ambiguity everywhere).
  • Hand-tuned one-off shades appearing ad hoc ("just this once") until the system dissolves into forty near-identical blues.

Build it once as described, and every future screen becomes assembly rather than invention — which is, in the end, what a design system is for.

Put it into practice

Try the techniques from this guide in our free tools — no sign-up required.

More in Web Design · All articles · FAQ

Keep reading