Build a brand color system with OKLCH (the 2026 way)
Why OKLCH replaces HSL for serious brand color systems, how to build a perceptually uniform scale, and how to ship it to Tailwind v4 and CSS variables.
Every brand color system used to start in HSL (hue, saturation, lightness). HSL is intuitive but perceptually broken — two colors with the same lightness value can look dramatically different to the human eye. The result: brand palettes that look harmonious in the designer's eyedropper and chaotic in production.
OKLCH (the OK Lab color space's polar form: lightness, chroma, hue) fixes this. Two colors at L=70% genuinely look equally bright. A scale built in OKLCH stays perceptually even from 50 to 950. Tailwind v4, Figma, and most modern design tools support it natively.
This tutorial shows you how to build a 10-step brand color scale in OKLCH, generate accessible semantic tokens from it, and wire the whole thing into a modern web stack without re-inventing your design system.
- A primary brand hue (you can pick from a hex value)
- A browser that supports oklch() — every major browser since 2023
- Optional: oklch.com or Huetone for visual tuning
Why HSL falls apart at scale
In HSL, yellow at lightness 60% looks much brighter than blue at the same 60%. That's because HSL treats all hues as if they have equal luminance, but the human eye is far more sensitive to greens and yellows than to blues and reds.
When you build a 10-step palette in HSL by linearly varying lightness, the yellow column ends up washed out and the blue column ends up muddy. Designers then hand-tune each value, the system loses its mathematical integrity, and the next person to extend it (or add a new hue) recreates the chaos.
OKLCH was designed specifically to fix this. Lightness in OKLCH maps to perceived brightness. Chroma is a perceptual saturation. Hue is degrees on a perceptual color wheel. A scale built once works for any hue.
The structure of a modern brand color system
Tier 1 — Primitives: a 10-step scale per hue (50, 100, 200, ..., 950). Even chroma drops at the extremes for accessibility. Built once, reused everywhere.
Tier 2 — Semantics: roles mapped to primitives. color.background = neutral.0, color.text = neutral.900, color.action = primary.600. Components only ever consume semantics.
Tier 3 — Mode variants: same semantic names, different primitive mappings for dark mode. color.background (dark) = neutral.950. No component code changes — just the mapping flips.
Step by step
- 01
Convert your brand hex to OKLCH
Use oklch.com or culori.js to convert. Example: #3b82f6 → oklch(62% 0.214 259). Note the hue value — that's your anchor.
- 02
Build a 10-step lightness scale
50: L=97%, 100: 92%, 200: 85%, 300: 75%, 400: 65%, 500: 58%, 600: 50%, 700: 42%, 800: 32%, 900: 22%, 950: 13%. Keep hue constant; tweak chroma slightly down at the extremes (50, 950) to avoid neon edges.
- 03
Generate a neutral scale at the same lightness steps
Same lightness values, but chroma close to 0 (try 0.005). This gives you a perceptually-matched gray scale — UI grays that don't feel disconnected from your brand color.
- 04
Add accent and status hues
Repeat the scale for accent (a complementary hue), success (green, ~145°), warning (amber, ~80°), and danger (red, ~25°). Same lightness math, same chroma drops at the extremes.
- 05
Define semantic tokens
--color-bg: var(--neutral-50); --color-text: var(--neutral-900); --color-action: var(--primary-600); --color-action-hover: var(--primary-700); --color-border: var(--neutral-200);
- 06
Wire into Tailwind v4 with @theme
In your CSS: @theme { --color-primary-500: oklch(58% 0.18 259); --color-primary-600: oklch(50% 0.20 259); } Tailwind automatically generates bg-primary-500, text-primary-600, etc.
Tip — Use OKLCH directly in @theme — Tailwind v4 supports it natively. No conversion step needed. - 07
QA contrast for accessibility
Every text-on-background combination must hit WCAG AA (4.5:1 for body, 3:1 for large text). Use a tool like contrast.tools or the built-in DevTools contrast checker. Adjust lightness, not hue, to fix failures.
Watch out — OKLCH lightness ≠ WCAG contrast. They're related but not identical — always verify with a real contrast tool, not by eye. - 08
Build dark mode by remapping semantics
@media (prefers-color-scheme: dark) { --color-bg: var(--neutral-950); --color-text: var(--neutral-50); --color-action: var(--primary-400); } Components don't change — only the mapping flips.
Key takeaways
- OKLCH gives you perceptually uniform color scales — HSL doesn't.
- Three-tier structure (primitives → semantics → mode variants) makes dark mode and rebrands trivial.
- Always verify contrast with WCAG tools, not visually.
- Tailwind v4 supports OKLCH natively via @theme.
Frequently asked questions
+Should I migrate an existing HSL system to OKLCH?
If your current palette works visually, you can leave primitives in HSL and add OKLCH for new hues. But if you've been hand-tuning to fix HSL's inconsistencies, a clean OKLCH rebuild will pay off within weeks.
+What about P3 wide-gamut color?
OKLCH supports P3 natively — values outside sRGB are clamped on legacy displays and rendered fully on modern ones. Use sRGB-safe chroma values (under ~0.22) if you want consistent appearance everywhere.
We design logos, brand kits, type systems and color palettes end-to-end.
Book a branding call