You pick two colors you like, write a linear-gradient between them, and the middle turns to sludge. A vivid blue and a vivid yellow meet in a dead gray-green that neither color suggested. Nothing is broken — the browser did exactly what you asked. The problem is what "between two colors" means mathematically, and it is fixable once you can see it.

Why the middle goes gray

By default a CSS gradient interpolates in sRGB, channel by channel. Halfway between the two colors, each of red, green and blue is the average of the two endpoints.

Take blue #0000FF and yellow #FFFF00. Average the channels and you get #808080 — mid gray. Both endpoints are fully saturated; the midpoint has no saturation at all.

This happens whenever the two colors sit on opposite sides of the color wheel, because the straight line between them in RGB space passes near the achromatic center. The further apart the hues, the worse the dip.

Colors that are close on the wheel do not have this problem. Blue to purple stays vivid throughout, because the line between them never approaches the middle of the space.

Fix one: add an intermediate stop

The most reliable fix, and the one that works everywhere. Rather than letting the browser find its own path, tell it to travel through a color you chose:

/* dips through gray */
background: linear-gradient(90deg, #0066FF, #FFCC00);

/* stays vivid */
background: linear-gradient(90deg, #0066FF, #00CCAA 50%, #FFCC00);

Pick the intermediate stop by walking the shorter way round the hue wheel. Blue to yellow can go through green or through magenta — both are valid, and they produce completely different gradients. Choose deliberately.

A quick way to find a candidate: put the two endpoints into the color mixer, look at what the naive midpoint is, then use the color wheel to pick a hue at that angle with the saturation restored. That is your middle stop.

Fix two: interpolate in a better color space

Modern CSS lets you choose the space the interpolation happens in:

background: linear-gradient(in oklab, #0066FF, #FFCC00);
background: linear-gradient(in oklch longer hue, #0066FF, #FFCC00);

Interpolating in OKLAB avoids most of the gray dip, because the straight line between two colors in a perceptual space does not pass through the desaturated center in the same way. OKLCH goes further by interpolating hue as an angle, so the gradient sweeps around the wheel rather than cutting across it — which produces a rainbow-like transition rather than a muddy one. The longer hue and shorter hue keywords control which way round it goes.

Support for this is good in current browsers but not universal. Because a browser that does not understand the syntax ignores the entire declaration, layer a plain fallback first:

.hero {
  background: linear-gradient(90deg, #0066FF, #00CCAA 50%, #FFCC00);
  background: linear-gradient(in oklab, #0066FF, #FFCC00);
}

The transparent-fade bug

A separate and very common artifact. This looks reasonable:

background: linear-gradient(to bottom, #3B82F6, transparent);

In older rendering paths it produces a gradient that fades through gray on its way out, because transparent is historically shorthand for rgba(0, 0, 0, 0) — transparent black. Interpolating toward it drags the color toward black even though the alpha is also falling.

Fade to a zero-alpha version of the same color instead:

background: linear-gradient(to bottom, rgb(59 130 246 / 1), rgb(59 130 246 / 0));

Modern browsers handle premultiplied alpha correctly and largely avoid this, but the explicit form costs nothing and removes the question entirely. It is the single most common gradient bug in production CSS.

Banding, and how to hide it

Gradients across large areas — full-page backgrounds, big hero sections — often show visible stripes rather than a smooth blend. This is quantisation: 8 bits per channel gives 256 levels, and a subtle gradient stretched over 1400 pixels has to repeat each level many times over, producing bands the eye picks up as edges.

Three mitigations, roughly in order of usefulness:

Increase the color distance. Banding is worst when the endpoints are very close together, because fewer distinct levels are available. A gradient with more range bands less.

Overlay fine noise. A very subtle noise texture breaks up the bands by dithering the boundaries. A small tiled PNG or an SVG feTurbulence filter at low opacity works, and this is what most polished gradient backgrounds are quietly doing.

Add intermediate stops. More stops give the renderer more anchor points and can reduce the effect, though it does not fix the underlying bit depth.

Direction, and the thing everyone gets backwards

In CSS, 0deg points up and angles increase clockwise. So 90deg runs left to right and 180deg runs top to bottom. This differs from several design tools and from the mathematical convention, which is why exported gradients frequently arrive mirrored.

Keyword syntax is harder to get wrong: to right, to bottom right, to top. There is one subtlety worth knowing — to bottom right guarantees the gradient line ends exactly at the corner regardless of the element's aspect ratio, whereas a fixed 135deg does not. On a wide element the two produce visibly different results.

The gradient generator shows the angle and the live preview together, which is faster than reasoning about it.

Radial and conic, briefly

radial-gradient() blends outward from a point. The useful controls are shape and size: circle versus ellipse, and keywords like closest-side or farthest-corner that determine where the gradient ends relative to the box. Defaults produce an ellipse sized to the farthest corner, which is rarely what people picture.

conic-gradient() sweeps around a center point rather than outward from it. It is what makes pie charts, loading spinners and color wheels possible in pure CSS, and with hard stops it produces crisp segments with no blending at all.

Restraint

A few practical limits. Text on a gradient is hard to keep accessible, because the contrast ratio changes across the element — check the text against the worst point of the gradient, not the average, in the contrast checker. Large blurred gradients can be expensive to repaint during scroll on lower-end devices. And a gradient that spans a wide hue range reads as decorative; one that spans a narrow range reads as a surface. Most interfaces want the second.

For the theory behind why interpolation spaces differ, our color models guide covers what each space is actually measuring, and the CSS color guide goes into the newer color functions in more depth.