Color, considered.
Pick a colour. Read it as HEX, RGB, HSL, HSV, OKLCH, or CMYK. Generate a tonal scale. Check contrast against black and white. Blend two colours by midpoint. Everything in your browser; nothing leaves the tab.
Eleven shades, one hue.
Same H and S, varying L. Tailwind-style 50 → 950 ramp. Click any shade to copy its hex.
WCAG, at a glance.
Required ratios: 4.5 for normal text (AA), 3.0 for large text (AA), 7.0 for AAA. Pick a colour above; we score it both ways.
Two colours, a midpoint.
Linear RGB blend. Click any swatch to copy. Drop the picked colour into either side with the buttons.
Why OKLCH
is the right colour space.
RGB describes how a screen makes light. Three channels, additive, easy to mix in code, hostile to humans — #33ff66 tells you the bytes, not the perception.
HSL / HSV were designed in the 1970s as a friendlier face on RGB. Hue is the colour wheel; saturation is intensity; lightness is how light. The win is intuitiveness; the catch is that they're not perceptually uniform — a "20% lightness shift" looks very different in yellow than in blue.
OKLCH (Björn Ottosson, 2020) fixes that. It's a perceptually-uniform colour space: equal numerical changes in L map to equal perceived changes in lightness, regardless of hue. Building a tonal ramp in OKLCH gives you shades that look evenly stepped to a human; doing the same in HSL produces ramps that look great at some hues and broken at others. Modern design systems (Tailwind 4, Radix Colors, OpenProps) all moved to OKLCH for exactly this reason.
The browser support arrived in 2023. Use oklch() directly in CSS today; the values are the same ones in the panel above.
The 4.5 number, explained.
WCAG 2.1's contrast ratio is the relative luminance of the lighter colour over the darker, plus a constant: (L1 + 0.05) / (L2 + 0.05). Pure black on pure white scores 21:1. The thresholds:
AA · normal text: 4.5:1. Most regulators (US ADA Title II, EU Web Accessibility Directive) treat this as the legal floor for body text. AA · large text: 3:1 — applies to text 18pt+ regular or 14pt+ bold. AAA: 7:1 normal, 4.5:1 large. Aspirational; very few brands hit it system-wide.
Pure mathematical contrast doesn't perfectly match perception, especially at the extremes. WCAG 3 (still in draft) uses APCA — a perception-tuned formula. For now, 4.5:1 is the bar; design above it.
Read
further.
- Ottosson · 2020A perceptual color space for image processingThe original OKLab/OKLCH derivation. Math-heavy but readable.
- W3C WAIWCAG 2.1 Contrast (Minimum)The legal-grade reference for AA contrast. Read once before declaring colour decisions done.
- oklch.comOKLCH live pickerA focused tool for OKLCH alone, with gamut visualisation. Worth bookmarking.
- MyndexAPCA — the WCAG 3 contrast modelThe next-generation contrast formula. More conservative on dark text; less so on white-on-bright.
Different jobs, different coordinates.
A color space is a way to describe a color with numbers. RGB describes how much red, green, and blue light to mix — convenient for screens because it matches how pixels work, but a poor model of human perception. HSL rotates that description into hue, saturation, and lightness, which feels more designer-friendly but also doesn't match perception (a 50% lightness yellow is much brighter than a 50% lightness blue). OKLCH, defined by Björn Ottosson in 2020, is perceptually uniform — equal numeric distance corresponds to equal perceived distance — which makes it the right tool for generating palettes. CMYK is the print equivalent of RGB, subtracting cyan, magenta, yellow, and black from white paper. Lab is a perceptually-motivated space older than OKLCH, used heavily in scientific imaging.
For everyday web work, the relevant spaces are RGB (because that's what the screen shows), HSL (because that's what designers typically reach for), and OKLCH (because that's what produces good interpolations and palettes). HEX is just RGB written in hexadecimal — the same six values stored as #RRGGBB. The distinction between sRGB and Display-P3 (the wider gamut Apple devices have used since 2017) matters increasingly: a color expressed as color(display-p3 1 0 0) is a more saturated red than #ff0000, but the two render identically on sRGB-only displays.
CSS Color Module Level 4 (still progressing through the standards process but widely supported) added oklch(), oklab(), color(), color-mix(), and relative-color syntax. These let you do operations that used to require build-time tooling: blend two colors at runtime with color-mix(in oklch, var(--a), var(--b)), rotate a hue with oklch(from var(--accent) l c calc(h + 30)). The browser landscape caught up in 2023; by 2024, every major browser shipped support.
| Space | Coordinates | Best for |
|---|---|---|
| sRGB / HEX | R, G, B (0–255) | screen rendering, hex codes |
| HSL | H, S, L (0–360, 0–100, 0–100) | quick designer-friendly tweaks |
| OKLCH | L, C, H (0–1, 0–~0.4, 0–360) | palettes, gradients, perception-correct edits |
| OKLab | L, a, b | color difference, deltaE-style math |
| Display-P3 | R, G, B in P3 gamut | vivid displays, photo color, brand vibrancy |
| CMYK | C, M, Y, K (0–100) | print prep |
Why your palettes never look right.
HSL is appealing because it sounds intuitive: a hue, how saturated, how light. But the math is naive. The lightness coordinate is computed without weighting the perceptual brightness of different hues. Yellow at 50% L is much brighter than blue at 50% L; orange and purple are wildly different at the same nominal lightness. If you generate a tonal scale by varying L from 10 to 90 in HSL, the visual brightness curve is jagged — some steps barely shift, others jump. Designers compensate by manually tweaking each step, which defeats the purpose of generative palettes.
OKLCH fixes this. The L coordinate is perceptual: 0.5 looks like the same brightness regardless of hue. A scale generated by stepping L from 0.1 to 0.9 produces visually-even intervals. Hue rotation in OKLCH preserves perceived lightness; in HSL, rotating the hue from yellow (60°) to blue (240°) drops the apparent lightness by a substantial amount even though H is the only variable changing. Every modern design system that ships generative tooling — Material You, Adobe's tonal palettes, Radix Colors, Tailwind v4's defaults — uses OKLCH or a perceptually-motivated equivalent under the hood.
The practical implication: if you're hand-picking colors and tweaking until they look right, HSL is fine. If you're generating colors algorithmically — a 10-step tonal scale from a single brand color, a complementary palette, dark-mode variants — use OKLCH. The conversion between them isn't trivial (it goes through XYZ as an intermediate), but every modern color library handles it. The Color tool on this page exposes both spaces; switch to OKLCH for any operation that involves arithmetic.
Make two color scales — one in HSL, one in OKLCH — both ramping lightness 10 to 90 in equal steps, both at the same hue. Look at them side by side. The HSL ramp will look uneven (some steps too dark, others too light); the OKLCH ramp will be visually smooth. Once you see it, you can't unsee it.
WCAG contrast, and what comes next.
The Web Content Accessibility Guidelines define contrast as a numeric ratio between the relative luminance of foreground and background. The threshold for normal text at AA is 4.5:1; for large text (18pt+ or 14pt+ bold) the bar drops to 3:1. AAA tightens to 7:1 / 4.5:1. The luminance formula is a weighted sum of the linearised sRGB channels — the linear-light versions of the gamma-encoded sRGB values you see in hex codes. The math is straightforward but easy to get wrong; libraries like colorjs.io, culori, and chroma.js handle it correctly.
WCAG 2.x's contrast formula has known weaknesses. It overestimates contrast for dark-on-dark combinations (a #333 on #111 reports as failing AA but reads fine) and underestimates for very light backgrounds. APCA (Accessible Perceptual Contrast Algorithm), proposed for WCAG 3.0, takes spatial frequency, dark-mode, and stem-thickness into account. APCA is gradually being adopted by design-system tooling; the W3C standardisation process is slow but APCA has appeared in Adobe products, the GOV.UK design system, and several Tailwind extensions since 2023.
For now, ship contrast checks against WCAG 2.x because that's what auditors use. Consider also running APCA in parallel for forward-compatibility — when WCAG 3 lands, sites that already pass APCA will need fewer remediation passes. The contrast checker on this page reports both AA and AAA scores; the underlying calculation uses the WCAG 2.x formula. The Contrast Checker tool drills into that specifically.
Operationally, integrate contrast into the design process at three stages. At design time, every Figma plugin (Stark, Able, Contrast) checks pairs in real-time. At build time, Storybook addons and CI tools (axe-core, pa11y, lighthouse-ci) catch failures before deploy. At runtime, CSS color-contrast() (Level 6, draft) lets the browser pick a foreground that hits a target ratio against a dynamic background; until that ships universally, runtime contrast adaptation requires JavaScript.
From one brand color to a working system.
A design system needs more than a single brand color. It needs tonal scales (the same hue at 10–12 lightness levels for elevation, hover states, disabled), semantic colors (success, warning, error, info), and dark-mode variants for everything. Generating these by hand is a full-time job; generating them programmatically is a couple of OKLCH operations.
The standard tonal-scale recipe: take the brand color in OKLCH, hold the hue and chroma roughly constant, vary the lightness. Material You uses 13 stops; Tailwind v4 uses 11; Radix uses 12 with carefully-tuned chroma per hue (saturated colors clip at extreme lightnesses, so chroma has to ramp down). The reason Tailwind redid its color palette in v4 (2024) was to switch from HSL-derived scales to OKLCH-derived scales — the new scales render visually-uniform across the full ramp, where the v3 scales had visible bumps.
For complementary palettes, rotate the hue by 180°; for triadic, by ±120°; for analogous, by ±30°. These rotations work cleanly in OKLCH because the L and C remain unchanged, so the rotated colors are perceptually balanced. Doing the same operation in HSL produces complementary pairs that feel visually mismatched because the rotated hue has different perceived brightness.
For dark mode, the naive flip — invert lightness — works for greys but breaks for accents. A blue at L 0.5 in light mode becomes a blue at L 0.5 in dark mode by just keeping the original; you typically want it slightly more saturated and slightly lighter to maintain perceived prominence on a dark background. Modern systems generate paired light/dark token sets where each semantic token (background, surface, primary, accent, etc.) has a curated value for each mode rather than a mechanical inversion.
Linear interpolation has a color space.
A gradient between two colors is interpolation. In what space you interpolate matters. A linear-RGB interpolation from red (#ff0000) to green (#00ff00) passes through a muddy brown at the midpoint — not because brown is between them perceptually, but because the math averages each channel independently. The same gradient in OKLCH passes through a vivid yellow, which is what most viewers expect because yellow is perceptually between red and green on the rainbow.
CSS exposes this directly: linear-gradient(in oklch, red, green) interpolates in OKLCH; the bare linear-gradient(red, green) defaults to sRGB. The gradient quality difference is dramatic for color pairs separated by significant hue distance. For monochromatic gradients (varying lightness within one hue), the difference is small. The new in OKLCH syntax is supported in Chrome 113+, Safari 16.4+, Firefox 113+ — universally available since 2023.
The same logic applies to color-mix(). color-mix(in oklch, red 50%, blue 50%) produces a balanced purple; color-mix(in srgb, red 50%, blue 50%) produces a darker, muddier purple because the sRGB midpoint is biased by the gamma curve. For any procedural color manipulation — generating hover states, computing disabled-state opacities, blending icon colors with backgrounds — choose OKLCH unless you have a specific reason to need sRGB.
Tokens, themes, runtime switching.
Modern design systems express colors as tokens — semantic names like --surface-primary, --text-on-accent — rather than hex codes scattered through component files. The token's value lives in one place; switching themes is a matter of redefining the tokens. CSS custom properties (var(--token)) make this trivial: define tokens at :root for light mode, override at [data-theme="dark"] for dark mode, let cascade do the rest.
For larger systems, hand-maintaining hundreds of tokens across multiple themes becomes impractical. Style Dictionary (Amazon), Theo (Salesforce), and TokenStudio plug into Figma to generate cross-platform tokens — emit CSS variables for web, JSON for React Native, XML for Android. Tailwind v4's CSS-only configuration moves the token definition into @theme blocks, which is a partial alternative to a separate build step.
For runtime theming beyond light/dark, the typical pattern is to compute a complete OKLCH-derived scale from a single user-selected primary color. Polaris, GitHub Primer, and Radix all do this. The user picks a brand color; the system generates 100, 200, 300, …, 950 stops via OKLCH lightness ramp, plus dark-mode variants. The result is a consistent palette that doesn't require the user to pick 13 colors by hand.
For shipped CSS, the trade-off between OKLCH and sRGB is browser support and predictability. OKLCH is universally supported but not all CSS tools (older PostCSS pipelines, some Sass loaders) understand the syntax. Hex codes round-trip through every tool ever made. The pragmatic choice: author tokens in OKLCH for math, generate hex codes for the actual CSS that ships. The Color tool on this page does both — pick or paste a color, see it in every notation.