OKLCH Design Tokens

Scalable color system

When I set out to define the color system for this project, I wanted something that would be both flexible and future-proof. Traditional hex or RGB values work fine for static palettes, but they become limiting when you need to adjust lightness, contrast, or accessibility on the fly. That’s why I chose to build the palette using the OKLCH color model. Unlike HSL or HEX, OKLCH is perceptually uniform — meaning that changes in lightness or chroma behave more consistently with how our eyes actually see color. This makes it much easier to fine-tune shades, generate accessible contrasts, and maintain harmony across the palette.

OKLCH / 04 × 11

Four hues. One system.

From light to dark: each row keeps its chroma and hue. Only lightness (L) changes. The outlined step 50 is the base color.

Primary C 0.15 · H 272.0°
  1. 0 L 90%
  2. 10 L 80%
  3. 20 L 70%
  4. 30 L 60%
  5. 40 L 50%
  6. 50Base L 40%
  7. 60 L 30%
  8. 70 L 20%
  9. 80 L 10%
  10. 90 L 5%
  11. 100 L 0%
Secondary C 0.13 · H 10.92°
  1. 0 L 95%
  2. 10 L 85%
  3. 20 L 73%
  4. 30 L 55%
  5. 40 L 40%
  6. 50Base L 30%
  7. 60 L 25%
  8. 70 L 20%
  9. 80 L 17%
  10. 90 L 12%
  11. 100 L 8%
Teal C 0.1099 · H 198.0°
  1. 0 L 99%
  2. 10 L 90%
  3. 20 L 85%
  4. 30 L 75%
  5. 40 L 70%
  6. 50Base L 65%
  7. 60 L 58%
  8. 70 L 45%
  9. 80 L 32%
  10. 90 L 24%
  11. 100 L 18%
Alert C 0.1659 · H 119.42°
  1. 0 L 99%
  2. 10 L 93%
  3. 20 L 91%
  4. 30 L 87%
  5. 40 L 83%
  6. 50Base L 80%
  7. 60 L 76%
  8. 70 L 60%
  9. 80 L 50%
  10. 90 L 40%
  11. 100 L 20%

0–100 names the token steps, not the L value. The browser maps out-of-gamut colors to the display; contrast still needs checking for each pairing.

/* Primary Palette */
--primary-color: oklch(40% 0.15 272);
--color-primary-0: oklch(from var(--primary-color) 90% c h);
--color-primary-10: oklch(from var(--primary-color) 80% c h);
--color-primary-20: oklch(from var(--primary-color) 70% c h);
--color-primary-30: oklch(from var(--primary-color) 60% c h);
--color-primary-40: oklch(from var(--primary-color) 50% c h);
--color-primary-50: var(--primary-color);
--color-primary-60: oklch(from var(--primary-color) 30% c h);
--color-primary-70: oklch(from var(--primary-color) 20% c h);
--color-primary-80: oklch(from var(--primary-color) 10% c h);
--color-primary-90: oklch(from var(--primary-color) 5% c h);
--color-primary-100: oklch(from var(--primary-color) 0% c h);

The foundation of the palette is the primary color range, which I created by varying lightness from 90% down to 0% while keeping chroma and hue constant. This gave me a smooth spectrum of tints and shades derived from one anchor hue. From there, I added a secondary color, an alert color for states and notifications, and a more playful accent color (teal). Each range follows the same principle — start with a base and build out a scale by adjusting lightness in OKLCH.

To make the palette practical for development, I translated every step into CSS custom properties. Each color in the scale gets a variable name (e.g. –color-primary-30, –color-teal-70) that identifies its position in the scale, rather than its lightness percentage. On top of that, I created utility classes (e.g. .bg-primary-30, .text-teal-70) to apply the colors directly in markup. This approach keeps design tokens consistent, makes it easy to swap or extend colors later, and reduces guesswork when working with the palette in real-world layouts.

/* Secondary Palette */
--secondary-color: oklch(30% 0.13 10.92);
--color-secondary-0: oklch(from var(--secondary-color) 95% c h);
--color-secondary-10: oklch(from var(--secondary-color) 85% c h);
--color-secondary-20: oklch(from var(--secondary-color) 73% c h);
--color-secondary-30: oklch(from var(--secondary-color) 55% c h);
--color-secondary-40: oklch(from var(--secondary-color) 40% c h);
--color-secondary-50: var(--secondary-color);
--color-secondary-60: oklch(from var(--secondary-color) 25% c h);
--color-secondary-70: oklch(from var(--secondary-color) 20% c h);
--color-secondary-80: oklch(from var(--secondary-color) 17% c h);
--color-secondary-90: oklch(from var(--secondary-color) 12% c h);
--color-secondary-100: oklch(from var(--secondary-color) 8% c h);
/* Teal Palette */
--teal-color: oklch(65% 0.1099 198);
--color-teal-0: oklch(from var(--teal-color) 99% c h);
--color-teal-10: oklch(from var(--teal-color) 90% c h);
--color-teal-20: oklch(from var(--teal-color) 85% c h);
--color-teal-30: oklch(from var(--teal-color) 75% c h);
--color-teal-40: oklch(from var(--teal-color) 70% c h);
--color-teal-50: var(--teal-color);
--color-teal-60: oklch(from var(--teal-color) 58% c h);
--color-teal-70: oklch(from var(--teal-color) 45% c h);
--color-teal-80: oklch(from var(--teal-color) 32% c h);
--color-teal-90: oklch(from var(--teal-color) 24% c h);
--color-teal-100: oklch(from var(--teal-color) 18% c h);
/* alert Palette */
--alert-color: oklch(80% 0.1659 119.42);
--color-alert-0: oklch(from var(--alert-color) 99% c h);
--color-alert-10: oklch(from var(--alert-color) 93% c h);
--color-alert-20: oklch(from var(--alert-color) 91% c h);
--color-alert-30: oklch(from var(--alert-color) 87% c h);
--color-alert-40: oklch(from var(--alert-color) 83% c h);
--color-alert-50: var(--alert-color);
--color-alert-60: oklch(from var(--alert-color) 76% c h);
--color-alert-70: oklch(from var(--alert-color) 60% c h);
--color-alert-80: oklch(from var(--alert-color) 50% c h);
--color-alert-90: oklch(from var(--alert-color) 40% c h);
--color-alert-100: oklch(from var(--alert-color) 20% c h);