Why hex gradients go grey in the middle
CSS Color 4 makes Oklab the default space for blending colours, with one exception kept for old pages: when every stop is a legacy sRGB colour — a hex code, a name, rgb() or hsl() — the browser must blend in gamma-encoded sRGB. Nearly every gradient on the web is written in hex, so nearly every gradient takes the straight line through the RGB cube, and between colours on opposite sides of the hue circle that line passes near grey. Blue #2563eb to orange #f97316 keeps 29% of its chroma at the midpoint in sRGB and all of it in OKLCH.
Writing in oklch after the direction opts out of the exception. Chrome and Edge have supported it since version 111, Safari since 16.2 and Firefox since 127 (June 2024). For anything older, the export carries the same path baked into extra hex stops, behind an @supports test rather than a second declaration — a custom property cannot fall back by cascade, because an unsupported value in one is accepted when declared and only fails where it is used.
Banding, and which browsers hide it
A gradient that changes slowly has fewer 8-bit colours than it has pixels, so each colour repeats across a band. The slate hero #0f172a to #1e293b changes colour 50 times across 1440 pixels, leaving bands up to 72 pixels wide. Whether you see them depends on the renderer. Each row below was measured on 8 October 2026 by screenshotting that gradient at 1440 × 40 and counting identical pixels along three rows.
| Renderer | Dithering, measured | in oklch from |
|---|---|---|
| Chromium (Chrome, Edge) | Dithers: rows differ, widest run 3–48px depending on the row | 111 |
| WebKit (Safari) | Dithers: rows differ, widest run 8–20px | 16.2 |
| Firefox | Not measured here; a bug to dither has been open for years | 127 |
| Canvas, image exports, video | Whatever the renderer does; assume no dither | — |
Grain at 2–4% does the dithering itself, in every renderer, for about 360 bytes of CSS. Above about 8% it stops being a dither and becomes a visible texture.
What a popular preset list measures
To see how often the problems above occur in practice, the tool's engine was run over all 382 presets in uiGradients (MIT licence), painted the way its own CSS paints them: hex stops, evenly spaced, left to right.
| Measured | Presets | Note |
|---|---|---|
| Lose over half their chroma through the middle (grey dead zone) | 38 of 382 | All 38 keep it in OKLCH |
| Cannot carry 4.5:1 text in white or black anywhere on them | 146 of 382 | Median scrim to fix: 31% black |
| Pass 4.5:1 on average while failing at the worst pixel | 146 of 382 | The average is the figure most checkers give |
| Bands 8px or wider across a 1440px hero, undithered | 325 of 382 | 68 of them 24px or wider |
Every preset in this tool passes all three checks: its text colour clears 4.5:1 over every pixel, it keeps its chroma through the middle, and where it would band it carries grain. A test fails the build if one stops passing. For colour work that is not a gradient, the contrast checker and the ramp and token generator use the same colour engine.