You build a header gradient in the design tool. Two dark blues, top to bottom, perfectly smooth on the canvas. You paste the rule into the theme, load the page, and the gradient has grown stripes: four or five flat bands with a visible edge between each one.
Nothing was lost on the way. The gradient is exactly the one you drew. What changed is the distance it has to cover. Take a real pair of colours, #101828 to #1D2A44. Red moves 13 levels, green 18, blue 28. Twenty eight is the largest of the three, so the whole ramp contains 29 distinct colours and not one more. Spread 29 colours across a 1600 pixel header and each one owns 57 pixels of screen. That is not a gradient, it is a staircase with very wide steps.
Banding is arithmetic, not craft. Once you can count the steps in a colour pair you know, before you ship, whether the gradient will be smooth and which of the three real fixes you need.
The builder below does the counting. Pick the type, set the angle, edit the stops, and it reports how many distinct 8 bit steps the gradient produces, how wide one step lands, and which pair of stops will band. Then it hands you the CSS, and a PNG if you need a file.
Where the steps come from
A browser stores each channel of a colour in 8 bits: 256 values per channel, 0 to 255, with nothing in between. There is no 40.5 in the blue channel, not on an ordinary display path. A gradient is therefore not a continuous ramp. It is the shortest staircase joining your two colours, and the number of stairs is fixed by the colours themselves, not by the size of the element you paint them into.
You can work it out on paper. Take the absolute difference in each channel and keep the largest: steps = max(|dR|, |dG|, |dB|). Black to white gives 255. The pair #3B66FF to #6E91FF gives 51, because blue is pinned at 255 in both and only red travels far. Two dark navies usually land between 20 and 40. Divide the on screen length of the gradient by that number and you have the width of one band.
Above roughly 3 pixels per band the edges start to show, and your visual system helps them along. Mach banding is the effect: lateral inhibition exaggerates the boundary between two nearly identical flat areas, so a step of one level out of 256 reads as a drawn line rather than a change of colour.
CSS gradient generator
Build a linear, radial or conic gradient, see exactly where it will band, then copy the CSS or save a PNG. Click the bar to drop a stop where you click and drag the handles to move them. Everything is drawn in this browser tab and nothing is uploaded.
Reading the gradient.
A click on the bar adds a stop right there, in the colour the gradient already has at that point, so nothing changes until you move it. Drag a handle to shift a stop. Double click a handle, or press Delete while it holds focus, to remove it. Arrow keys nudge the focused handle by one per cent, with Shift by ten.
0 points up, 90 points right, 180 points down.
Banding happens when too few 8 bit steps are stretched over too many pixels. A tiled noise layer at about 6 per cent opacity breaks the step edges up. Switching it on adds the extra lines to the CSS below and bakes the same noise into the PNG.
What the readout under the preview counts
The step count is measured, not guessed from the hex values. The tool samples the resolved gradient along the axis it is painted on, at the size the preview is on your screen, and counts distinct rounded RGB triples. Axis length depends on the type: linear uses the gradient line length for that angle and box, the width times the sine of the angle plus the height times the cosine; radial uses the radius out to the farthest corner; conic uses the circumference, because the colour travels around the circle.
The warning is narrower than the step count, deliberately. It fires when one adjacent pair of stops produces bands 3 pixels or wider across a gap of at least 8 per cent of the axis: close in colour, far apart in space, which is the exact recipe for a stripe. Two identical stops are a flat segment rather than a gradient, so they are not counted at all.
Because the measurement uses the real width of the preview, the number moves with the viewport and is recomputed on resize. The default blue ramp warns at 4.5 pixel bands in an 800 pixel preview and comes up clean at 2.3 pixels when the same preview is 380 pixels wide on a phone. That is not sloppiness, it is the gradient: the same colours band across a desktop hero and stay clean inside a mobile card.
All of it happens in your browser tab. Nothing is uploaded, there is no server round trip, and nothing is stored between visits.
Dark, flat and wide is the worst case
Three properties make banding visible, and they tend to arrive together. The first is a short colour distance. Subtle ramps are subtle because the two colours sit close together in code values, which means few steps. #FFFFFF to #F2F4F8, the ramp people use to lift a section off a white page, moves 13 levels in red, 11 in green and 7 in blue. Thirteen steps across a 1600 pixel section is one band every 123 pixels.
The second is flat colour. Photographs band too, but grain and detail break the edges up before you notice. A flat gradient has nothing to hide behind, which is why the complaint is always a hero section, a card background or a rendered sky, never a portrait.
The third is the screen. Plenty of laptop panels are 6 bit with frame rate control: 64 real levels per channel, the rest faked by flickering between two neighbours. On those displays a 28 step ramp can arrive as seven or eight bands no matter how clean your CSS is. You cannot fix somebody else’s panel, which is the argument for the fix that works on all of them.
Three fixes, and the one that works everywhere
The first fix people reach for is more stops, and it is mostly wrong. Adding a stop halfway between the two colours creates no levels, because none were missing: the ramp already passes through every value between the endpoints. An intermediate stop helps only when it changes the path or the pacing. Routing through a colour that moves a different channel adds genuine steps. Pulling the outer stops inwards shortens the run, so from 0 and 100 per cent to 20 and 80 per cent on a 1600 pixel box turns 57 pixel bands into 34 pixel bands. Better, still visible, and now with a flat edge at each end.
The second is a wider colour space. CSS Color 4 lets you interpolate somewhere other than sRGB, as in linear-gradient(in oklab, #101828, #1D2A44), and it genuinely changes gradients: it is the cure for the muddy grey midpoint two complementary hues produce in sRGB. It is not a cure for banding. The result is still quantised to whatever the display path offers, which is 8 bits per channel on most machines. A 10 bit path helps, but only when panel, operating system and browser all agree, which you cannot know from the server.
The third is noise, and it is the one that works. Add a small random variation per pixel and the boundary between two steps stops being a straight line: some pixels on the light side fall dark, some on the dark side fall light, and the edge dissolves into texture the eye reads as a smooth transition. This is dithering, the same trick that let 256 colour GIFs carry photographs. It needs no bit depth, no colour management, no recent browser and no cooperation from the panel.
The Banding fix checkbox is that idea done in CSS. It stacks a tiled 160 pixel SVG feTurbulence layer at 6 per cent opacity above the gradient, and the extra background-image, background-repeat and background-size lines appear in the CSS output. Over a flat 128 grey patch it takes the variance from zero to roughly plus or minus 3 levels, enough to break a step edge apart. It is not colour neutral: it lifts a flat colour by about two levels, so look at a dark ramp again after switching it on. The PNG export uses per pixel random noise of about plus or minus 2 levels instead of the tiled SVG, so the two match in effect, not pixel for pixel.
/* the shape of the dithered output: noise layer first, gradient underneath */
background-image:
url("data:image/svg+xml,... feTurbulence ..."),
linear-gradient(160deg, #101828 0%, #1D2A44 100%);
background-repeat: repeat, no-repeat;
background-size: 160px 160px, auto;
The angle points the gradient line, not the light
In linear-gradient(45deg, ...) the angle is the direction the gradient line points, which is the direction of the last colour. 0deg points to the top, 90deg to the right, and the angle increases clockwise, so 45deg starts at the bottom left and finishes at the top right. Most people expect the opposite, because they are picturing a light source in the top left corner. The default reinforces the confusion: with no angle at all a linear gradient runs to bottom, which is 180deg.
The keyword forms are not the angles they look like either. to top right means 45deg only when the box is square. The specification puts the gradient line perpendicular to the line joining the two neighbouring corners, so on a 1600 by 200 hero to top right resolves to 7.1 degrees, almost straight up. Rendered against linear-gradient(7.125deg, ...) the two are identical to within one level of noise. The tool works in numbers only: the angle field wraps modulo 360, so 400 becomes 40 and -30 becomes 330, and eight presets cover the usual directions.
That angle drives linear and conic gradients, and for a conic gradient it is the from angle, with the colour turning clockwise from there. Radial gradients have no angle, so the field is switched off with a line of text saying why, rather than sitting there ignoring you. Radial previews and PNG exports size to the farthest corner, which is what CSS does by default.
Stop positions carry one more trap. The CSS output keeps the numbers you typed, but a browser never lets a stop sit behind the one before it: the later stop is clamped up to the earlier one. Typing 70 per cent and then 20 per cent gives a hard edge at 70 per cent, not a reversal, and the preview, the PNG and the readout all use that resolved order. Hex fields take three or six digits with or without the hash, write themselves back in full form on commit, and turn red and are ignored while the text is not a colour.
/* stop two sits behind stop one, so the browser clamps it */
background: linear-gradient(90deg, #3B66FF 70%, #6E91FF 20%, #FFFFFF 100%);
/* what you get: solid blue to 70%, a hard edge, then a ramp to white */
A gradient should almost never be an image file
Everything above assumes the gradient stays as CSS. Export the same ramp as a JPEG and it gets measurably worse, for a reason worth knowing.
Here is the measurement, on the ramp from the top of this article. Painted as a CSS gradient across 1600 pixels and screenshotted, one row of pixels holds 80 distinct values with no flat run longer than 3 pixels: Chromium dithers gradients as it paints them, which is why the screen beats the 29 colour theory. Push those same pixels through JPEG at quality 80 and the row collapses to 28 distinct values in 28 flat runs, the widest 136 pixels across. At quality 60 it is 20 values and a 200 pixel run. The encoder does not merely fail to help. It removes the dithering the browser had already done.
The mechanism is the block transform. JPEG cuts the image into 8 by 8 blocks and describes each block as a sum of cosine patterns. A gentle ramp inside one block is carried by coefficients of very small amplitude, and small coefficients are the first thing the quantiser rounds to zero. Once they are gone the block reverts to its average colour and goes flat, which is precisely a band. Chroma subsampling at 4:2:0 halves colour resolution in both directions on top of that, so a gradient that shifts hue fares worst of all. The comparison of WebP, AVIF, JPEG and PNG covers what each encoder keeps and discards.
WordPress then repeats the damage. Upload that JPEG and core builds the whole size set from it, thumbnail, medium, medium_large, large and every size the theme registers, re-encoding each one through WP_Image_Editor at a default quality of 82, or 86 for WebP. Anything wider than the 2560 pixel big_image_size_threshold also gets a re-saved -scaled original. That is five or more files from one upload, each a fresh pass of the same quantiser. srcset then hands a different one of them to every visitor, so the file you inspected is not the file most people see.
Against all that, the CSS rule is 49 bytes, makes no HTTP request, needs no srcset, no retina copy and no regeneration, and repaints at any container size. There is almost no case for the file. When there is one, an email template, a PDF, a print layout or an OG image that has to be a real file, use Download PNG: PNG is lossless, so the noise survives, the default is 1600 by 900, and each dimension clamps between 16 and 8000 pixels.
If the library already holds a dozen exported gradient JPEGs from before you knew any of this, the awkward part is not deleting them, it is working out which posts still point at them. That is what the usage analysis in WunderPaint’s media library manager is for.
What the tool leaves out on purpose
Stops are hex only, so there is no alpha and no rgba. A gradient that fades to transparent is one line of hand written CSS anyway. There are no interpolation hints, the bare percentage between two stops that shifts the midpoint, and no oklab or oklch interpolation. No repeating gradients, no stacking of several gradients into one background, no dragging stops around the preview, and nothing is remembered between visits because nothing is stored. Stops cap at eight and floor at two, so Add stop switches off at eight and the remove buttons switch off at two.
Those omissions matter in exactly one situation: a hue path with a bad middle. Blue to yellow through sRGB passes a dead grey, and the fix there is in oklch or a deliberate intermediate stop, not noise. Everything else the tool covers: two to eight opaque stops, linear, radial or conic, an angle, an honest measurement and a file when you want one.
Back to the header
Count first. Two colours, the largest channel difference, the width of the element: that is the entire prediction and it takes ten seconds. If the answer is more than about 3 pixels per band you know what the page will look like before you load it, and you know it for the desktop width specifically, because the same ramp inside a narrow card is fine.
Then choose deliberately instead of nudging colours until they look acceptable on your monitor. Shorten the run or move the stops if the layout allows it. Change the colours if the design allows it. If neither is available, and with dark ramps neither usually is, switch the noise on and accept a two level shift in flat colour that nobody will notice, in exchange for edges nobody will notice either.
And keep it as CSS. A gradient is 49 bytes of instruction that the browser paints at the size the container turns out to be, with its own dithering applied for free. Turning it into a JPEG throws that dithering away, adds a request, adds five files to the media library, and hands a lossy encoder a picture composed entirely of the one thing it is built to discard.