Anatomy of an 11-Stop 50–950 Color Scale
Modern UI design systems popularized by Tailwind CSS use an 11-stop numeric scale:
`50 ➔ 100 ➔ 200 ➔ 300 ➔ 400 ➔ 500 ➔ 600 ➔ 700 ➔ 800 ➔ 900 ➔ 950`
- Each stop serves a dedicated semantic UI role:
- 50–100: Soft tints, badge backgrounds, table row hover states, alert banners.
- 200–300: Input borders, disabled buttons, subtle divider lines, secondary badges.
- 400–500: Primary brand elements, focus rings, interactive toggles, active tab highlights.
- 600–700: Hover button states, accessible body text on light backgrounds.
- 800–950: Dark mode surface backgrounds, high-contrast headings, elevated cards.
Step 1: Establishing the 500 Seed Anchor
Start by selecting your primary brand HEX code (e.g. `#0284c7` Sky Blue). In your design system scale, this seed color typically anchors at Stop 500 or Stop 600.
Convert your seed anchor to OKLCH: ```css /* #0284c7 converted to OKLCH */ --color-brand-500: oklch(0.58 0.158 241.5); ```
Step 2: Interpolating OKLCH Lightness Curves
To ensure smooth visual progression without visual jumps, space out OKLCH Lightness (`L`) values systematically:
| Stop Key | Target OKLCH Lightness (`L`) | UI Functional Purpose |
|---|---|---|
| 50 | `97%` | Card hover tint / Alert background |
| 100 | `93%` | Subtle pill badge background |
| 200 | `85%` | Secondary border / Divider |
| 300 | `75%` | Form input border default |
| 400 | `66%` | Secondary button icon |
| 500 | `58%` | Primary Brand Anchor |
| 600 | `50%` | Button hover state / Light mode text |
| 700 | `42%` | Accessible body text |
| 800 | `33%` | Dark mode card background |
| 900 | `23%` | Header background / Dark surface |
| 950 | `14%` | Page body background (Dark mode) |
Step 3: Managing Chroma At Scale Extremes
A common mistake when generating scales is keeping Chroma (`C`) constant across all stops.
At extreme lightness values (Stop 50 near 97% lightness, or Stop 950 near 14% lightness), high chroma causes harsh glowing or muddy tones.
Rule of Thumb: Taper Chroma down by 60%–80% at stops 50 and 950 to keep surfaces elegant and readable.
Author Insight: System Token Architecture
Author Insight by Abhay Vachhani (Full Stack Engineer): > In scalable design systems, never hardcode numeric stops directly inside feature components. Always create semantic layer tokens (e.g. `--btn-primary-bg: var(--color-brand-500)`) so that white-label themes or seasonal dark mode tweaks can be applied globally in CSS without touching component code. > > By decoupling raw color scales (`--color-blue-500`) from semantic tokens (`--color-interactive-primary`), design system updates can be rolled out across hundreds of frontend microservices with zero risk of breaking existing UI contracts.
Step 4: Exporting to Tailwind v4 @theme CSS Variables
Once generated, plug your palette directly into your project's main stylesheet:
@theme { --color-brand-50: oklch(0.97 0.020 241.5); --color-brand-100: oklch(0.93 0.050 241.5); --color-brand-200: oklch(0.85 0.080 241.5); --color-brand-300: oklch(0.75 0.120 241.5); --color-brand-400: oklch(0.66 0.145 241.5); --color-brand-500: oklch(0.58 0.158 241.5); --color-brand-600: oklch(0.50 0.150 241.5); --color-brand-700: oklch(0.42 0.130 241.5); --color-brand-800: oklch(0.33 0.100 241.5); --color-brand-900: oklch(0.23 0.070 241.5); --color-brand-950: oklch(0.14 0.040 241.5); } ```
Step 5: Mapping Semantic Tokens to Functional UI Roles
In production web applications, define a semantic token layer above your raw scales:
:root {
/* Light Mode Semantic Mapping */
--bg-app: var(--color-slate-50);
--bg-surface: #ffffff;
--text-primary: var(--color-slate-900);
--text-muted: var(--color-slate-600);
--border-default: var(--color-slate-200);
--brand-action: var(--color-brand-600);@variant dark { /* Dark Mode Semantic Mapping */ --bg-app: var(--color-slate-950); --bg-surface: var(--color-slate-900); --text-primary: var(--color-slate-100); --text-muted: var(--color-slate-400); --border-default: var(--color-slate-800); --brand-action: var(--color-brand-500); } ```