Why your CSS gradient has a gray band in the middle
Blue to yellow should pass through green. In sRGB it passes through mud. The cause is a straight line drawn through the wrong color space, and the fix is now one keyword of CSS.
Write this and look at the middle:
background: linear-gradient(90deg, #0000ff, #ffff00);
Blue to yellow. You expect it to sweep through green. Instead there is a gray brown band sitting in the center like a stain. This is not a browser bug. It is the default behavior working exactly as specified, and it has a one word fix.
What the browser is doing
By default a CSS gradient interpolates in sRGB, channel by channel, in a straight line.
Halfway between #0000ff and #ffff00 is:
R: (0 + 255) / 2 = 128
G: (0 + 255) / 2 = 128
B: (255 + 0) / 2 = 128
#808080. Mid gray. Not approximately gray, exactly gray. The math is not making a
mistake; the color space is. sRGB is a description of what a monitor can emit, not a map
of how color is perceived, and a straight line through it does not follow any path the eye
would call natural.
The effect is worst between hues that are far apart and both saturated. Blue to yellow is the textbook case. Red to cyan and purple to green do it too. Two colors close in hue barely show it, which is why plenty of gradients look fine and you only notice the problem when you reach for something bold.
The fix
CSS Color Level 4 lets you name the interpolation space:
background: linear-gradient(in oklab 90deg, #0000ff, #ffff00);
in oklab puts the straight line through a perceptually uniform space instead. The midpoint
lands somewhere with actual chroma, and the ramp keeps its saturation from end to end. This
is supported in all current major browsers.
Two spaces are worth knowing:
- OKLab is Cartesian. It is the safe default: it keeps chroma up and takes the shortest visual route between two colors.
- OKLCH is polar, so it travels around the hue wheel rather than cutting across it.
This is what you want for rainbow style ramps and color wheels. You can steer it with
shorter hue,longer hue,increasing hueordecreasing hue.
/* Cuts across: blue to yellow through a muted green */
background: linear-gradient(in oklab 90deg, #0000ff, #ffff00);
/* Goes around: blue → cyan → green → yellow, chroma held high */
background: linear-gradient(in oklch longer hue 90deg, #0000ff, #ffff00);
Supporting older browsers
Older engines throw away a declaration they cannot parse, so pair the two:
.hero {
/* Sampled sRGB approximation, for engines without color space support */
background-image: linear-gradient(90deg, #0000ff 0%, #2a3ad4 20%, #4d63a8 40%,
#7f8f74 60%, #c2c14a 80%, #ffff00 100%);
/* Real thing, for everyone else */
background-image: linear-gradient(in oklab 90deg, #0000ff, #ffff00);
}
The first rule bakes extra stops along the perceptual path so that even simple sRGB interpolation between them stays close to the intended curve. Twelve stops gets you within a shade or two. The CSS gradient generator writes both lines for you and shows the two ramps side by side so you can see how much the color space is buying you.
Two more things that go wrong
Fading to transparent. Do not do this:
background: linear-gradient(to right, red, transparent); /* dirty */
In some engines transparent resolves to rgba(0, 0, 0, 0), which is transparent black,
so the gradient drifts towards gray as it fades. Fade to the same color at zero alpha
instead:
background: linear-gradient(to right, rgb(255 0 0 / 1), rgb(255 0 0 / 0)); /* clean */
Banding on large areas. A gradient across a wide hero at 8 bits per channel will show
visible steps, especially in dark blues and grays. Adding a very subtle noise overlay, such
as a tiny tiling PNG or an SVG feTurbulence filter at low opacity, hides it completely.
Dithering beats increasing the bit depth you do not control.
The takeaway
The gray band is a straight line drawn through a space that was never meant for drawing straight lines. Name a perceptual color space and the problem disappears. It is one of the rare cases where the correct answer is also the shorter one to type.