CSS Gradient Banding Fix: 4 Ranked Methods

You build a smooth gradient, ship it, and on a wide hero section you see ugly horizontal stripes where the color is supposed to fade seamlessly. Those stripes are color banding, and they show up most on subtle gradients across large areas. This is a focused troubleshooting guide: first why banding happens, then four fixes ranked from best to worst, each with a paste-ready snippet.

Why CSS gradients band

Standard sRGB color uses 8 bits per channel, giving 256 levels per channel. When a gradient spreads a small color difference across many pixels, the browser has to step between adjacent integer values because there is nothing in between. Each step is a flat block of one color, and the boundary between blocks reads as a visible line.

Two things make this worse. First, low-contrast gradients (a near-black to slightly-less-black fade, or a pale tint of one hue) have very few distinct values to work with, so each band is wide. Second, human vision exaggerates the effect through Mach banding: our eyes enhance contrast at edges, so we perceive a bright or dark fringe at each step that is not actually in the pixel data. That is why a gradient can look mathematically correct in a color picker yet still appear striped on screen.

Per the W3C CSS Images specification, gradients are interpolated in a working color space, and browsers rasterize the result to the 8-bit framebuffer for most displays. The banding is a quantization artifact of that final 8-bit step, not a bug in your CSS. Knowing this tells you the goal of every fix below: break up or hide the hard quantization boundaries.

Fix 1 (best): an SVG noise overlay

The most robust fix is to add a faint layer of noise on top of the gradient. Noise dithers the image: it scatters pixels slightly above and below each color step so the boundaries dissolve, the same trick print and video have used for decades. You generate the noise with SVG's feTurbulence filter and drop it in as a CSS background, so there is no extra HTTP request and nothing to download.

Place the noise layer first in the background shorthand (top of the stack) and your gradient underneath. Two details make this snippet actually render: the <svg> needs a viewBox so the <rect> has a size to fill, and the noise is desaturated to gray and faded with a low alpha so it dithers subtly instead of dumping colored static onto the page.

.hero {
  background:
    url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.12'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
    linear-gradient(180deg, #1a1a2e, #16213e);
}

The fractalNoise type with a high baseFrequency gives fine grain rather than coarse clouds. The feColorMatrix strips the color so you get neutral gray dither, and the feFuncA slope sets how strong it is: raise it toward 0.2 if banding still shows, lower it toward 0.05 if the grain is too obvious. The MDN documentation for feTurbulence explains each attribute. Run the data URI through our SVG optimizer if you want it as compact as possible before pasting it into production CSS.

Fix 2: more color stops

If you cannot add an overlay, give the browser more anchor points to interpolate between. Adding intermediate color stops shortens the distance over which each 8-bit step is stretched, so individual bands become narrower and harder to see. You are not eliminating quantization, just spreading it more evenly.

background: linear-gradient(
  180deg,
  #1a1a2e 0%,
  #182040 25%,
  #172a4d 50%,
  #16213e 100%
);

Pick the in-between colors by interpolating in a perceptual space rather than guessing hex values. The trade-off: this helps smooth, low-contrast gradients but does little for very subtle near-grayscale fades where there simply are not enough distinct 8-bit values to assign. Use a color converter to translate stops between hex, RGB, and HSL while you tune them, and see OKLCH vs HSL vs hex for CSS colors for why a perceptual space produces more even intermediate steps.

Fix 3: layered semi-transparent gradients

You can stack two or more partly transparent gradients so their banding boundaries land in different places and partially cancel. Because the bands no longer align, no single hard edge dominates, and the combined result reads smoother than either layer alone.

background:
  linear-gradient(180deg, rgba(26,26,46,0.5), rgba(22,33,62,0)),
  linear-gradient(180deg, rgba(22,33,62,0), rgba(22,33,62,1));

This needs no images and no filters, so it is the lightest of the CSS-only options. It is fiddlier to tune than adding stops, and the improvement is modest, so reach for it when an overlay is not an option and extra stops were not enough on their own.

Fix 4 (last resort): a dithered image

When you need a guaranteed result across every browser and the gradient is purely decorative, export it as a pre-dithered raster image. An image editor can apply ordered or error-diffusion dithering, baking the noise into the pixels so banding never appears at runtime. A PNG keeps the dither pattern intact; avoid lossy JPEG, which can reintroduce blocking.

The cost is real: an extra network request, a fixed resolution that will not scale crisply to every viewport, and a file you must regenerate whenever the design changes. Treat this as the fallback only when the three CSS approaches above are not enough.

Per-browser reality check

Banding is partly an engine-rendering issue, so the same CSS can look different across browsers. In practice Safari tends to render gradients the smoothest, while Firefox is the one where banding most often remains visible after CSS-only tweaks, since it lacked built-in gradient dithering for years. Chromium sits in between. Always test your fix in Firefox specifically; if it looks clean there, it will almost certainly look clean elsewhere. Also check on a real low-brightness or wide-gamut display, since banding that is invisible at full brightness can reappear in a dark room. When you have settled on stops, rebuild the final gradient in our CSS gradient generator to grab clean, copy-ready CSS.

Which one to use

Start with the noise overlay (Fix 1): it addresses the root quantization cause and works on subtle gradients where extra stops fail. If an overlay is impractical, add intermediate stops (Fix 2), then layer transparent gradients (Fix 3) if you still see lines. Drop to a dithered image (Fix 4) only for decorative gradients that must look identical everywhere. Verify every change in Firefox before you call it done.

Frequently Asked Questions

Design tools often preview in higher bit depth or apply their own dithering, while the browser rasterizes to an 8-bit sRGB framebuffer. That final quantization to 256 levels per channel is what introduces the visible steps, so a gradient can look perfect in an editor yet band once rendered as CSS.

Partially. More stops shorten the distance each 8-bit step is stretched across, so bands get narrower and less noticeable. But it cannot create color values that do not exist, so very subtle near-grayscale gradients still band. For those, a noise overlay is the more reliable fix.

Minimal. The feTurbulence noise is generated as an inline SVG data URI, so there is no extra HTTP request and nothing to download. The filter is computed once by the browser. Keep numOctaves low and the alpha slope subtle, and the rendering cost stays negligible on modern devices.

Each browser engine handles gradient interpolation and rasterization slightly differently, and Firefox historically lacked gradient dithering. In practice Safari renders gradients the smoothest and Firefox shows banding most readily. Always test your fix in Firefox, since a result that looks clean there will almost always look clean in other browsers.

Not by itself. OKLCH gives more perceptually even interpolation between stops, which helps the spacing of bands look more uniform, but the output is still quantized to 8-bit sRGB on most displays. You still need dithering or extra stops to remove the visible steps entirely.