Color Scale Algorithm
How Tokens generates professional, perceptually balanced color scales
Overview
Tokens uses a sophisticated dual-algorithm approach to generate color scales that are both aesthetically pleasing and functionally superior for UI design. The system automatically detects whether a color is chromatic (has hue) or achromatic (neutral/gray) and applies the appropriate algorithm.
Why Two Algorithms?
Different colors have different needs in UI design:
- Chromatic colors (blues, greens, oranges, etc.) benefit from vibrant mid-tones and smooth transitions
- Achromatic colors (grays, neutrals) need subtle light shades for backgrounds and high-contrast dark shades for text
The OKLCH Foundation
Both algorithms use OKLCH color space, which provides perceptually uniform color manipulation. This means equal numeric changes produce equal visual changes—something RGB and HSL can't guarantee.
L - Lightness (0-1)
Perceived brightness from black (0) to white (1). Unlike HSL, lightness values match human perception.
C - Chroma (0+)
Colorfulness or saturation. Higher values = more vibrant. Can exceed 0.37 for very saturated colors.
H - Hue (0-360°)
Color angle: 0° = red, 120° = green, 240° = blue, etc.
Learn more at oklch.com
Chromatic Color Algorithm
For colors with hue (blues, greens, oranges, purples, etc.), we use a three-part approach inspired by Matt Ström's WCAG-driven color palette generation.
Lightness steps are calibrated to provide visually even progression from light to dark, optimized for UI elements like buttons, cards, and badges.
Shade Offset from base (500) 50 +0.33 (much lighter) 100 +0.28 200 +0.22 300 +0.15 400 +0.07 500 0.00 (exact base color) 600 -0.10 700 -0.20 800 -0.28 900 -0.34 950 -0.38
Chroma follows a parabolic curve that peaks at shade 500, creating vibrant mid-tones while reducing saturation at extremes for natural-looking lighter and darker shades.
Formula: C(n) = -4(max-min)n² + 4(max-min)n + min
Where n = normalized position (0-1)
max = 1.1 (10% boost at peak)
min = 0.3 (70% reduction at extremes)This ensures colors remain vibrant where they matter most (buttons, links, accents) while preventing oversaturation in backgrounds and dark UI elements.
Lighter colors appear to shift hue perceptually—a phenomenon called the Bezold-Brücke effect. We compensate by slightly rotating hue in lighter shades.
Formula: H(n) = H_base + 5(1 - n) Where n = normalized position (0-1) Example: If base hue = 210° (blue) Shade 50: 215° (slightly warmer) Shade 500: 210° (exact base) Shade 950: 210° (stays true)
This subtle adjustment (±5°) keeps colors looking consistent across the entire scale, preventing the "muddy" or "off" appearance common in naive algorithms.
Reference
This approach is inspired by Matt Ström's article on generating WCAG-compliant color palettes for Stripe:
Generating Color Palettes →Achromatic Color Algorithm
For neutral colors (grays with chroma < 0.01), we use a distribution pattern inspired by Tailwind CSS that prioritizes readability and subtle UI backgrounds.
Neutrals need different behavior than chromatic colors. Light shades must be very subtle (close to white) for card backgrounds and subtle borders, while dark shades need aggressive contrast for readable text.
Shade Offset from base (500) Step Size Purpose 50 +0.429 0.015 Subtle backgrounds 100 +0.414 0.048 Very light UI elements 200 +0.366 0.052 Light borders/dividers 300 +0.314 0.162 Card backgrounds 400 +0.152 ⚡HUGE Hover states 500 0.000 (base) 0.117 Base neutral 600 -0.117 0.068 Muted text 700 -0.185 0.102 Secondary text 800 -0.287 0.064 Body text (dark mode) 900 -0.351 0.060 Headings 950 -0.411 Deep backgrounds
Notice the massive lightness drops between shades 300-500. This creates excellent contrast for text on light backgrounds while keeping the lighter shades subtle and non-distracting.
Real-World Validation
This distribution closely matches Tailwind CSS's neutral scale, which has been battle-tested across thousands of production applications:
Tailwind neutral-500 in OKLCH: oklch(0.556 0 0) Our algorithm at base 0.556: 50: 0.985 (vs Tailwind 0.985) ✓ 400: 0.708 (vs Tailwind 0.708) ✓ 950: 0.145 (vs Tailwind 0.145) ✓
Reference
This approach is based on analysis of Tailwind CSS's color system:
Building a Tailwind-Ready Color System →Benefits of This Approach
OKLCH ensures equal lightness changes produce equal visual changes. Shade 300 looks equally lighter than 400 as 700 looks darker than 600.
The parabolic chroma curve creates punchy, engaging colors for interactive elements like buttons and links without oversaturating backgrounds.
Neutral scales provide high-contrast text options while keeping light shades subtle enough for backgrounds and dividers.
Based on proven approaches from Tailwind CSS and Matt Ström's work at Stripe, used in thousands of production applications.
Hue shift compensation prevents the "muddy" or "off" appearance common in algorithmically generated scales.
Both chromatic and achromatic scales work together seamlessly, ensuring your entire design system feels cohesive.
Technical Implementation
A color is considered achromatic (neutral) if its chroma value is less than 0.01. This catches pure grays as well as colors that are nearly imperceptible from gray.
Regardless of algorithm, shade 500 always exactly matches your input color. This ensures your brand color appears precisely as intended in the generated scale.
The complete implementation is open source and available in our repository:
View oklch.ts on GitHub →