Bhinish Dhiman

How to make a CSS gradient that stays vivid, readable and free of bands

Write the gradient with in oklch after its direction, so stops from opposite sides of the colour wheel do not meet in grey, and give older browsers baked hex stops behind an @supports test. Then measure text at the least legible pixel under it, not the average, and fix failures with a darker stop, a different text colour or a scrim. Finally count the 8-bit bands along its length and add 2–4% grain if they are wide.

A gradient looks finished the moment it looks pretty, which is exactly when three faults are still hiding in it. The middle can go grey, the headline on it can vanish over one end, and a large screen can show it as a staircase of stripes. None of them shows up on a small swatch, and each one has a number you can check.

Make one with the tool

  1. Open the gradient generator. It starts on the Dusk preset, so there is a working gradient on screen from the first second.
  2. Press the space bar (or Generate in the bar at the bottom) for a new gradient. Each one it makes carries its text colour at 4.5:1 over every pixel and keeps its chroma through the middle, unless the colours you have locked make that impossible, in which case the Text chip says it fails. Click a stop’s padlock to keep that colour through the next press.
  3. Pick each colour in the chips on the gradient and drag its handle along the bar to move it. The + puts a new stop in the middle of the widest gap, already coloured as the gradient is there.
  4. Open Settings for the rest: Linear, Radial, Conic or Mesh; the angle or centre; Blend colours in, which you should leave on OKLCH; Hue path, set to Longer only if you want to travel the long way round the colour wheel; grain; exact stop positions with Reverse and remove; and the ten swatches under Presets, every one measured.
  5. Open the Text chip under the gradient. Set Text colour to test to the colour your headline will be; if black or white measures better, a button offers it. Switch the preview between Hero, Card and Button, and press Mark the worst pixel to see where a line fails.
  6. If the Bands chip says stripes, open it and press Add 3% grain to dither it.
  7. Press Copy CSS in the bar, or Export for Custom properties, Tailwind v4, Inline style or, for a mesh, an SVG download. The Code chip shows every format with its size in bytes.

The grey middle, and where it comes from

Interpolation space is the colour model a browser blends in to work out the colours between two stops. The CSS colour module, level 4, makes OKLab the default, with an exception kept for old pages: when every stop is written as a hex code, a name, rgb() or hsl(), the browser must blend in gamma-encoded sRGB. Almost every gradient on the web is written in hex, so almost every one takes the straight line through the RGB cube.

Between colours on opposite sides of the hue circle that line runs close to grey. Chroma is how far a colour sits from grey at its lightness, and it is the figure that drops:

Blue #2563eb to orange #f97316 Midpoint OKLCH chroma at its weakest Kept
Hex stops, no space given (sRGB) #8f6b81, a greyed mauve 0.055 29%
in oklch a saturated magenta-pink 0.187 100%

The fix is three words: linear-gradient(90deg in oklch, #2563eb, #f97316). Chrome and Edge have supported it since 111, Safari since 16.2 and Firefox since 127. For anything older the tool bakes the same path into extra hex stops — 12 for this pair, all within ΔE2000 0.89 of the real thing — and puts the real one behind @supports.

Use @supports rather than a second declaration when the gradient lives in a custom property. A custom property accepts any value when it is declared, so an old browser keeps --hero: linear-gradient(in oklch, …) and only fails where var(--hero) is used, leaving no background at all.

Measure the text at its worst point

A gradient behind a headline is not one background but hundreds. The headline has to be readable on the least favourable of them, so the number that matters is the lowest ratio under the text, not the average.

White text across navy #1e3a8a to pale yellow #fde68a shows why. The average ratio is 3.82:1, enough for large text on paper. The pixels range from 10.36:1 at the navy end to 1.25:1 at the yellow end, where the letters all but disappear. The tool reads every pixel under each text box, reports the lowest WCAG 2 ratio and the lowest APCA Lc with where each sits, and, when a line fails, gives the lightest black or white scrim that would lift it.

The scale of the problem is easy to underestimate. Run over all 382 presets in the uiGradients collection, painted as its own CSS paints them, 146 cannot carry 4.5:1 text in white or in black anywhere across their width. Those same 146 pass on average for one of the two colours, which is the figure a checker that samples a single colour would report.

Count the bands before they show

Banding is the staircase a slow gradient becomes once each pixel is rounded to one of 256 levels per channel. The dark slate hero #0f172a to #1e293b changes colour only 50 times across 1440 pixels, so its bands are up to 72 pixels wide.

Whether anyone sees them depends on the renderer. Measured on 8 October 2026, Chromium and WebKit both dithered that gradient on screen: neighbouring rows differed, breaking the band edges up. Firefox was not measured here; a bug asking it to dither gradients has been open for years. Canvas drawings, image exports and video encoders may not dither at all. Grain does the job everywhere: a noise layer at 2–4% nudges each pixel by about a level, which breaks the edges up for 366 bytes of CSS. Above about 8% it stops being a dither and becomes a visible texture.

Other ways to do it

There is no built-in operating-system method for any of this. In plain CSS you can write in oklch by hand in any modern browser, and Tailwind v4 already blends its gradient utilities in OKLab and takes /oklch as a modifier, as in bg-linear-90/oklch from-blue-600 to-orange-500. Neither measures the text on the result or the bands it will show.

Check that nothing left your device

The tool runs in the page. Open your browser’s developer tools, choose the Network tab and work for a minute: no request carries your colours, and the state lives in the address bar’s fragment, which browsers never send. Disconnecting from the network does not stop it working.

Where to go next

Open the gradient generator and start from a preset. If you are still choosing the colours themselves, the contrast checker finds the nearest passing colour at your brand hue, and OKLCH for designers explains the colour space the gradient blends in.