RGB vs HSL vs LAB vs CMYK: Which Color Model Should You Use, and When
Four color models, one practical answer: which to use in CSS, which to use in print, and why LAB quietly became the most important of all.
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.
Before generating anything, convert your seed into HSL to understand what you're working with. Take #2563EB → hsl(221°, 83%, 53%).
Three properties determine everything downstream:
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.
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.
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:
#F8FAFC#F1F5F9#E2E8F0#64748B (secondary text)#0F172AThis 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.
Semantics answer "what happened?" independently of brand identity:
hsl(142°, 71%, 45%)hsl(38°, 92%, 50%)hsl(0°, 72%, 51%)) and brand away from it.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.
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.
A proper ramp makes dark mode mostly mechanical:
primary-500 on light becomes primary-300/400 on dark, because saturated mid-tones vibrate against black and fail contrast.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.
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.
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.
Try the techniques from this guide in our free tools — no sign-up required.
More in Web Design · All articles · FAQ
Four color models, one practical answer: which to use in CSS, which to use in print, and why LAB quietly became the most important of all.
Sixty-plus hex-coded combinations grouped by what they're actually for — calm SaaS dashboards to loud streetwear — each with a note on why it works.
The color wheel, harmonies, temperature and contrast — explained for people who ship interfaces, not oil paintings.