OKLCH solves what HSL never could
HSL has been the workhorse of color manipulation on the web since the late 1990s. It's mathematically simple — a hue angle, a saturation percentage, a lightness percentage — and it powers virtually every “lighten 10%” preprocessor function ever shipped. It has one flaw that everyone ignores: HSL lightness is not perceptually uniform.
Plot HSL hsl(60, 80%, 50%) (yellow) next to hsl(240, 80%, 50%) (blue) at the same lightness value. They look nothing alike in brightness — the yellow burns your retinas while the blue retreats into the page. This is the same reason a Tailwind v3 palette generated naïvely in HSL produced inconsistent middle stops across hues, which is exactly why Tailwind v4 moved to OKLCH.
OKLCH (Björn Ottosson, 2020) is a transform of OKLab into cylindrical lightness-chroma-hue coordinates. Crucially, its lightness channel maps to perceived brightness — two OKLCH colors with the same L value look equally bright regardless of hue. The 500 stops across primary, secondary, accent, success, warning, danger, and info all feel like they belong on the same step of the same staircase.
The 11-stop perceptual scale
Eleven stops (50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950) is the de-facto modern standard. It aligns with Tailwind, Radix Colors, shadcn/ui, and most design-system token libraries. Each stop targets a specific OKLCH L value drawn from a perceptually-curved ramp:
| Stop | OKLCH L | Typical role |
|---|---|---|
| 50 | 0.985 | Subtle tint backgrounds, hover wash |
| 100 | 0.965 | Soft surfaces, tag fills |
| 200 | 0.925 | Disabled fills, divider tints |
| 300 | 0.870 | Borders, faint outlines |
| 400 | 0.780 | Muted iconography, large display |
| 500 | 0.680 | Mid-tone — brand anchor |
| 600 | 0.585 | Default primary button (light mode) |
| 700 | 0.500 | Hover state for primary buttons |
| 800 | 0.420 | Active / pressed states |
| 900 | 0.340 | Dark foreground text |
| 950 | 0.230 | Inky deep mode background |
Chroma follows a separate curve — it peaks around the middle stops (400–500) where saturation reads strongest, and tapers toward the extremes so 50 doesn't blow out and 950 doesn't feel muddy. Every generated stop is then gamut-mapped into displayable sRGB so the browser actually renders the color the math produced.
APCA vs WCAG 2.2 — what to grade against
WCAG 2.2's contrast ratio (1.4.3, 1.4.6) is the legal baseline. Body text needs 4.5:1, large text needs 3:1, non-text UI components need 3:1. The model is the same for dark text on light backgrounds as it is for light text on dark — a single symmetric luminance ratio.
That symmetry is APCA's biggest objection. Light text on dark backgrounds is perceived differently from dark on light (Stevens' power law, irradiance asymmetry, decades of human factors research). APCA computes Lc — a signed value approximately between −108 and +106 — that respects polarity. Negative Lc means light text on dark; positive means dark on light. The absolute value gets checked against thresholds:
| Use case | APCA Lc minimum | Equivalent WCAG |
|---|---|---|
| Body text (fluent reading) | 75 | 4.5 : 1 |
| Body text (preferred) | 90 | 7 : 1 |
| Large text (24px+ or 18.66px bold) | 60 | 3 : 1 |
| UI components (buttons, fields) | 60 | 3 : 1 |
| Focus rings, dividers, icons | 45 | 3 : 1 |
| Spot text / decorative | 30 | no minimum |
Our recommendation: pass WCAG 2.2 contractually, target APCA Bronze Simple as a forward-compatible bonus. The Studio displays both side-by-side so any pairing that's marginal in one model gets flagged.
Color-vision deficiency — the collapse you can't see
Approximately 8% of men and 0.5% of women have some form of color-vision deficiency. The three dichromacies — protanopia (no red cones), deuteranopia (no green cones), and tritanopia (no blue cones) — collapse hue differences that look obvious to typical vision. Success-green and danger-red are the textbook example: in deuteranopia, both reduce to muddy brown-yellow tones that are nearly indistinguishable.
The fix is twofold. First, never rely on color alone to convey information (WCAG 1.4.1) — pair the color with an icon, text label, or shape. Second, run your semantic colors through the Machado, Oliveira & Fernandes (2009) transforms and check that the pairs that need to be distinguishable still are, computed as ΔE-OK perceptual distance. The Palette Studio runs this check automatically and surfaces a warning when any semantic pair collapses below the distinguishability threshold.
One source of truth, every platform
Design tokens exist to keep the same hex value flowing through Figma, Tailwind, vanilla CSS, iOS, and Android. The W3C Design Tokens Community Group (DTCG) format is the emerging standard for that source of truth. From DTCG JSON you can transform to:
Tailwind v4@theme block with --color-* custom propertiesTailwind v3tailwind.config.ts with theme.extend.colorsVanilla CSS:root variables plus [data-theme="dark"] overrideTokens StudioFigma plugin import for design paritySwiftUIColor extension for UIKit / SwiftUIKotlin ComposeColor object for Android Jetpack
Build the transforms into your design-system release pipeline. When a color shifts, every consumer updates from the same commit. The Studio's export panel produces all of the above in one click.
Step-by-step workflow
Pick a base color in OKLCH, not HSL
Convert your brand hex to OKLCH. Note the L (lightness), C (chroma), and H (hue). OKLCH lightness is perceptually uniform — two colors at L=0.6 look equally bright to the human eye regardless of hue.
Generate 11 stops with a curved lightness ramp
Target L values that step evenly through the perceptual range — roughly 0.985, 0.965, 0.925, 0.870, 0.780, 0.680, 0.585, 0.500, 0.420, 0.340, 0.230 for stops 50 through 950. Chroma peaks around the middle stops and tapers at extremes.
Gamut-map each stop to displayable sRGB
OKLCH can describe colors outside the sRGB gamut. Use chroma clamping to bring each generated stop back into a renderable color while preserving lightness and hue.
Derive semantic tokens from scales
Map primary-600 to the rest-state of buttons, primary-700 to hover, primary-100 to soft surfaces. Define background, foreground, border, focus, success, warning, danger, and info as token references — never raw hex.
Grade every pairing with both WCAG and APCA
Body text on background, button text on button, focus ring on adjacent surface, disabled state, alert filled, alert tinted. Pass WCAG 2.2 contractually and check APCA Lc as forward-compatibility insurance.
Verify under color-vision deficiency
Run the palette through protanopia, deuteranopia, tritanopia, and grayscale simulations. Watch especially for success/danger collapse — that's the classic red-green failure.
Export tokens to every consumer
Ship the same source of truth as Tailwind theme, CSS variables, W3C DTCG JSON, Tokens Studio JSON for Figma, Swift UIColor, and Compose Color. Token references — not hex strings — keep all platforms in sync.
FAQ
Why OKLCH instead of HSL?
HSL is mathematically convenient but not perceptually uniform — two HSL colors with the same L value can look dramatically different in actual brightness. OKLCH (Björn Ottosson, 2020) is a perceptual color space derived from OKLab. An 11-stop scale generated in OKLCH steps evenly to the human eye across all hues; the same scale in HSL produces obvious bright spots and dark spots.
What is APCA and how does it differ from WCAG 2.2?
APCA (Accessible Perceptual Contrast Algorithm) is the contrast model proposed for WCAG 3. It computes Lc, a signed value typically between −108 and +106, that accounts for font size, font weight, and polarity (dark text on light vs light text on dark). WCAG 2.2 uses a luminance ratio (4.5:1 for body text) that does not distinguish polarity, so combinations that score 4.7:1 can still fail readability in dark mode — a problem APCA explicitly addresses.
Should I ship APCA today?
Use APCA as a secondary check, not a primary contractual baseline. WCAG 2.2 is the legal floor in most jurisdictions (Section 508, ADA Title II in the United States; EAA in Europe; AODA in Canada). APCA Bronze Simple thresholds (Lc ≥ 75 body, Lc ≥ 60 large, Lc ≥ 45 non-text) are the forward-compatible target that future-proofs your palette.
Why 11 stops? Why not 10 or 12?
Eleven stops (50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950) align with Tailwind, Radix, and shadcn conventions, so the output drops into existing ecosystems without renaming. The extra 50 and 950 stops give you a near-white tint and an inky deep stop without forcing you to choose between them.
How do I handle dark mode without inverting?
Don't invert. Use lighter stops (400–500) for primary surfaces in dark mode, darker stops (50–100) for the corresponding foregrounds. Each role — primary, secondary, accent, success, warning, danger, info — needs its own light-mode and dark-mode pair. Both should pass WCAG and APCA independently. Inverting a light palette produces dark mode that looks correct in marketing screenshots but fails real reading sessions.
What about color-vision deficiency?
Around 8% of men and 0.5% of women have some form of color-vision deficiency. The most common is deuteranopia (no green cones). Test your palette under all three dichromacies using Machado et al. (2009) matrices: protanopia, deuteranopia, and tritanopia. The classic failure is success (green) collapsing into danger (red) under deutan or protan. Pair color with iconography, text, or shape to satisfy WCAG 1.4.1 (Use of Color).
How do I export tokens once and use them everywhere?
Start with W3C Design Tokens Community Group JSON (DTCG) as your source of truth. From there, transform into platform-specific formats: Tailwind config (or v4 @theme block), CSS variables, Tokens Studio JSON for Figma, Swift UIColor extension, Kotlin Color object for Compose. Build the transform into your design-system release pipeline so every consumer gets the same hex value from a single edit.
Where does this end up under WCAG 3?
WCAG 3 is still a working draft. APCA is currently the contrast model under consideration, but the final spec may change. The safe strategy: pass WCAG 2.2 today, design with OKLCH and APCA so you're ready for whichever direction WCAG 3 goes, and version your design tokens so you can re-grade if requirements shift.
Build your color system in the Studio
Apply every step of this guide in one click: OKLCH 11-stop scales, WCAG + APCA grading, color-blindness simulation, state-aware report, and token exports to every platform.