Home / Snippets / Color & Theming /

Design system palette in OKLCH

10-step color scales with uniform lightness progression — one hue angle, infinite palette.

Blue (hue 265)
50
100
200
300
400
500
600
700
800
900
Green (hue 155)
50
100
200
300
400
500
600
700
800
900
Red (hue 25)
50
100
200
300
400
500
600
700
800
900
Widely Supported
colorno-js

Quick implementation

:root {
  /* Base hue — change this one value to generate a new palette */
  --hue: 265;

  /* 10-step lightness scale with uniform perceptual steps */
  --color-50:  oklch(0.97 0.02 var(--hue));
  --color-100: oklch(0.93 0.04 var(--hue));
  --color-200: oklch(0.85 0.08 var(--hue));
  --color-300: oklch(0.77 0.12 var(--hue));
  --color-400: oklch(0.69 0.16 var(--hue));
  --color-500: oklch(0.61 0.19 var(--hue));
  --color-600: oklch(0.52 0.19 var(--hue));
  --color-700: oklch(0.44 0.17 var(--hue));
  --color-800: oklch(0.35 0.13 var(--hue));
  --color-900: oklch(0.27 0.09 var(--hue));
}

/* Usage */
.btn-primary { background: var(--color-600); color: var(--color-50); }
.btn-primary:hover { background: var(--color-700); }
.surface { background: var(--color-900); color: var(--color-100); }
.badge { background: var(--color-200); color: var(--color-800); }

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building a design system color foundation.

Goal: Generate a 10-step color palette (50–900) from a single OKLCH hue angle, with perceptually uniform lightness steps and consistent chroma curves.

Technical constraints:
- Use oklch() for every color value — no hex, rgb(), or hsl().
- Lightness steps: 0.97, 0.93, 0.85, 0.77, 0.69, 0.61, 0.52, 0.44, 0.35, 0.27.
- Chroma peaks at the 500–600 range and tapers toward both extremes.
- Store the hue in a single --hue custom property so the entire palette can be re-themed by changing one value.
- Ensure 50–200 steps have sufficient contrast for dark text, and 600–900 for white text.

Framework variant (pick one):
A) Vanilla CSS custom properties on :root.
B) Tailwind v4 theme configuration — define the palette in @theme and extend with semantic aliases.

Edge cases to handle:
- Chroma clipping: some hue/chroma combos exceed the sRGB gamut — browser clamps automatically but verify in Chrome DevTools.
- Complementary palette: generate a second palette at hue + 180 for contrast/accent.
- Neutral palette: use chroma 0.01–0.02 for grays that have a subtle tint matching the brand hue.
- WCAG contrast: 500 is the pivot — pair 50–400 with dark text, 500–900 with white text.

Return CSS custom properties.

Why this matters in 2026

Design systems traditionally shipped hundreds of hand-tuned hex values. OKLCH changes the game: you define a single hue angle and derive the entire 10-step scale with uniform perceptual lightness. Changing the hue regenerates every shade instantly — no Figma re-export, no manual contrast checking. This is how Tailwind v4, Open Props, and modern design tokens work under the hood.

The logic

OKLCH separates color into three axes: lightness (0–1), chroma (saturation intensity), and hue (0–360). By fixing the hue and stepping lightness from 0.97 down to 0.27, you get a scale where each step looks equally spaced to the human eye — unlike HSL, where perceptual jumps are uneven. Chroma follows a bell curve, peaking at mid-lightness (500–600) and tapering at extremes, because very light and very dark colors can't sustain high saturation without clipping. The var(--hue) custom property makes the entire palette re-themeable with a single value change.

Accessibility & performance

The lightness scale is designed around WCAG contrast ratios: steps 50–400 pair with dark text (contrast ratio above 4.5:1), and steps 500–900 pair with white text. This eliminates guesswork when assigning foreground/background combinations. Performance-wise, custom properties resolve once at cascade time — there is no runtime cost compared to hardcoded values. The only caveat is gamut clipping: if a hue/chroma combination exceeds sRGB, the browser silently clamps it, which may shift the perceived color slightly. Use Chrome DevTools' color picker (it flags out-of-gamut values) to verify.