Deft
Ask AI

Search

Search skills, authors and topics

jakubkrehel / oklch-skill

oklch-skill

Practical OKLCH color workflow skill for building consistent, accessible, and tunable color systems in modern UIs.

Install

npx skills add https://github.com/jakubkrehel/oklch-skill --skill oklch-skill

Palette Generation

The scale convention

Design system palettes use a numeric scale from 50 (lightest) to 950 (darkest). The standard labels by palette size:

SizeLabels
5100, 300, 500, 700, 900
950, 100, 200, 300, 500, 700, 800, 900, 950
1150, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950

9 steps is the standard default (matches Tailwind).

Algorithm

Given a base color with lightness (L), chroma percentage, and hue (H):

Step 1 — Lightness bounds:

delta = 0.4minL = max(0.05, baseL - delta)maxL = min(0.95, baseL + delta)

Lightness is clamped to [0.05, 0.95] to avoid pure black/white which have zero chroma.

Step 2 — Distribute lightness evenly from maxL (lightest, label 50) to minL (darkest, label 950).

Step 3 — Clamp chroma per step. Each lightness level has a different maximum chroma for a given hue and color space:

maxChroma = findMaxChroma(step[i].L, hue, colorSpace)step[i].C = (chromaPercentage / 100) * maxChroma

This ensures every step is within gamut. High-chroma base colors will have lower chroma at the lightest and darkest ends — this is correct and expected.

CSS variable output

css
:root {  --color-50: oklch(0.971 0.012 250);  --color-100: oklch(0.932 0.028 250);  --color-200: oklch(0.882 0.048 250);  --color-300: oklch(0.812 0.078 250);  --color-500: oklch(0.623 0.188 250);  --color-700: oklch(0.445 0.138 250);  --color-800: oklch(0.362 0.108 250);  --color-900: oklch(0.289 0.078 250);  --color-950: oklch(0.215 0.048 250);}

Multi-hue palettes

When generating palettes for multiple hues, use the same lightness and chroma percentage for all. Same L guarantees equal perceived brightness. Same chroma percentage (not absolute chroma) guarantees equal vividness relative to each hue's maximum.

css
:root {  /* Same L, same C% (80% of max) — different absolute C per hue */  --blue-500: oklch(0.623 0.141 250);   /* 80% of max 0.176 */  --green-500: oklch(0.623 0.157 145);  /* 80% of max 0.196 */  --red-500: oklch(0.623 0.202 25);     /* 80% of max 0.253 */}

Different hues have different max chroma at the same lightness. Using the same absolute C value across hues would make some appear more vivid than others.

Dark mode

Reverse the palette mapping so that the lightest step becomes the darkest and vice versa:

css
:root {  --color-bg: var(--color-50);  --color-text: var(--color-950);}
.dark {  --color-bg: var(--color-950);  --color-text: var(--color-50);}

This works because oklch's perceptual uniformity means equal L steps in both directions produce equally readable results.

Why not HSL palettes?

Hue drift: hsl(240, 80%, 20%) and hsl(240, 80%, 90%) are not the same perceptual hue. The light variant shifts ~18° toward purple. OKLCH hue is stable.

Brightness inconsistency: hsl(60, 100%, 50%) (yellow) and hsl(240, 100%, 50%) (blue) have the same HSL lightness but wildly different perceived brightness.