Color Gradients in Design: How to Use Them Well

Quick Answer

Use color gradients by keeping them to two close colors, blending in a perceptually uniform space like Oklab so the middle does not go muddy, and checking text contrast at the darkest and lightest point rather than the average. One gradient per screen. Flat color should stay your default.

Good gradients follow three rules. Keep the two colors close together on the color wheel, blend them in a perceptually uniform space so the middle does not turn grey, and check your text contrast at the worst point of the gradient rather than the average. Do those and most gradients look intentional. Skip them and you get the muddy, slightly dated look that gives gradients a bad name.

This guide is about the design judgment, not the syntax. If you want the code for linear, radial and conic gradients, our CSS gradient guide covers every type with copy-paste examples, and MDN Web Docs' gradient reference documents the full syntax for every gradient function. What follows is how to decide what to put in them.

When should you use a gradient at all?

Start from the assumption that you should not. Flat color is faster to read, easier to keep accessible, and almost never looks wrong. A gradient needs a reason.

Good reasons: you want to draw the eye to one specific area, like a hero section or a primary button. You want to suggest depth without a shadow. You need to fill a large empty area that would look flat and cheap in a single color. Or you are covering a photo with an overlay so text sits readably on top of it, which is probably the most defensible use of all.

Bad reasons: everyone else is doing it. It looks modern. The flat version felt boring. That last one is worth sitting with, because a design that feels boring in flat color usually has a layout or hierarchy problem, and a gradient will not fix it. It will just make the same problem shinier.

One per screen is the working limit. Gradients work by pulling attention, so two of them compete and both lose.

Which colors actually work together in a gradient?

Neighbours. Colors that sit near each other on the color wheel blend cleanly because the path between them is short and stays saturated the whole way.

The reliable combinations, roughly in order of how hard they are to get wrong:

What to avoid: complementary pairs sitting directly opposite each other, like red to green or blue to orange. The straight line between them runs through grey, and you get a dead zone in the middle. If your brand genuinely needs both, put a third color between them to route the blend around the dead spot rather than through it. There is also a way to send the blend the long way around the color wheel without adding a stop at all, which is covered further down.

If you are building this into a brand system, our brand color guide covers how to pick the base palette first. Gradients should come out of the palette, not replace it.

Want to see a blend before you commit to it? Try the free gradient generator →

Which direction should a gradient run?

On small, bounded shapes like buttons, cards and badges, light at the top and darker at the bottom. That reads as raised. Flip it and the same shape tends to read as pressed in or hollow. The reason isn't taste. It's a shortcut your visual system takes before you've consciously looked at anything.

When your brain sees shading with no obvious light source, it assumes the light is coming from above. V. S. Ramachandran showed how strong that assumption is in a short paper in Nature in 1988. Shapes defined only by shading, with no outline or shadow, were seen as bumps or dents depending on which way the gradient ran. He also found the visual system assumes a single light source for the whole scene. That second part matters more for UI than people realise.

Why does a light top read as raised?

Because a bump lit from above is brighter on its upper half, and a dent is brighter on its lower half. Your eye reverses that logic to guess the shape. Kleffner and Ramachandran followed up in Perception & Psychophysics in 1992 and found this happens fast enough to cause pop-out. The time it took people to spot a single convex shape didn't go up as more shapes were added to the display. That's the same kind of instant, pre-attentive processing that makes a red dot jump out of a field of grey ones.

They also found that the effect depends on shading running top to bottom. Left and right differences in brightness didn't produce pop-out at all. So if you want a button to feel pressable at a glance, a vertical gradient does real work that a horizontal one doesn't.

Does the angle need to be exactly vertical?

No, and a slight tilt may even look more natural. Sun and Perona at Caltech asked where people expect the light to be and published the answer in Nature Neuroscience in 1998 under the title "Where is the sun?" People's preferred light direction wasn't straight overhead. It sat to the left, and it correlated with handedness. The authors noted the same leftward bias in paintings going back two thousand years.

In CSS terms, light from slightly left of overhead means a gradient running down and a little to the right. That's somewhere around linear-gradient(165deg, #818cf8, #4f46e5) rather than the default 180deg. Don't overthink the exact number. Anything that reads as "top, maybe top-left" will work.

When should you break the rule on purpose?

One honest limit. The assumption isn't fixed for life. Adams, Graf and Ernst showed in Nature Neuroscience in 2004 that the light-from-above prior shifts with experience. But it takes deliberate training to move it, and your users won't be doing that on your landing page. For the CSS syntax behind any of this, including angles and color stops, our CSS gradient guide covers the full reference.

Why does the middle of your gradient look muddy?

This is the single most common gradient problem, and it has a one-line fix that most designers have never been told about.

By default, a browser blends between two colors in sRGB. That is a rectangular color space, and the straight line between two saturated colors passes through a region of lower saturation. So your vivid pink and your vivid blue meet somewhere in the middle as a washed-out lavender-grey. The gradient is doing exactly what you asked. You just asked in the wrong color space.

The W3C's CSS Images Module Level 4 addresses this directly. It adds a color interpolation method to gradient functions, so you can name the space the blend runs through. The spec is explicit about why it matters, noting that choosing a polar rather than a rectangular colorspace for gradient interpolation avoids desaturation, because polar spaces like LCH and Oklch are inherently chroma-preserving.

In practice that means writing the space into the gradient:

background: linear-gradient(in oklab to right, #f01, #081);

The same two colors, blended through a space built to match how we actually perceive color, keep their intensity across the whole run. The spec's own example shows one pair of colors drawn three ways, in Lab, in sRGB and in Oklab, precisely because the difference is obvious once you see it side by side.

Worth knowing which spec defines what here, because they are two different documents. CSS Images 4 gives you the interpolation syntax, the part that lets you write the space into the gradient. The color spaces themselves, Oklab and Oklch included, are defined in the W3C's CSS Color Module Level 4. That is the reference to reach for when you want the actual definition of a space rather than the syntax that consumes it.

One thing to get right before you ship it, because the failure mode is nastier than you would expect. The interpolation syntax is newer than gradients themselves, and a browser that does not understand in oklab does not fall back to a normal gradient. It throws out the whole declaration as invalid, and you get no background at all. Not a duller gradient. Nothing.

The fix is two declarations, in this order:

background: linear-gradient(to right, #f01, #081); background: linear-gradient(in oklab to right, #f01, #081);

That is just the cascade doing its job. An older browser cannot parse the second line, drops it, and keeps the plain sRGB gradient from the first. A current browser understands both and the second one wins. Everybody gets a gradient, and the people on modern browsers get the good one. You do not need a feature query for this.

Both orderings of the arguments are legal, so in oklab to right and to right in oklab do the same thing. Pick one and be consistent.

If you take one thing from this article, take this. Most gradients people call ugly are not badly chosen, they are badly interpolated.

Can you fix a complementary gradient without a third color?

Usually yes, and it is worth knowing before you go adding stops.

Earlier I said that if your brand needs two complementary colors, you put a third color in between to route the blend around the grey dead zone. That still works. But there is a second route, and it comes from the same corner of the spec as the interpolation fix.

Polar color spaces like Oklch and LCH describe hue as a position on a wheel. Which means there are always two ways to travel from one hue to another: the short way round, or the long way. Blue to orange the short way cuts straight across the middle of the wheel, which is exactly where the muddy zone lives. Blue to orange the long way sweeps around the outside through green and yellow, staying saturated the whole trip.

The W3C's CSS Color Module Level 4 defines four keywords for choosing which arc you get:

In practice:

background: linear-gradient(to right, #06f, #f80); background: linear-gradient(in oklch longer hue to right, #06f, #f80);

Two things will save you an afternoon here.

First, hue keywords only do something in a cylindrical polar space. That means Oklch, LCH, HSL and HWB. Write in oklab longer hue and you have asked for an arc in a space that has no hue axis to travel along, because Oklab is rectangular. So the pairing matters: in oklab for a clean straight blend, in oklch plus a hue keyword when you want to control the route.

Second, name the arc you want rather than leaving it to the default. You will be reading this code again in six months, and "I meant the long way" is not something you can infer from its absence.

One edge case worth knowing. In polar spaces the hue of a fully achromatic color, meaning white, black or a pure grey, is undefined, and the spec treats it as powerless or missing. So a gradient running from a saturated color to white does not have two hues to arc between, and a hue keyword will not do what you expect. If one end of your blend is a neutral, you are back to the straight interpolation case.

And the same fallback discipline applies as before. Plain gradient first, enhanced gradient second, so an older browser drops the line it cannot parse and still paints something.

Whether the long way round is the right design call is a separate question. It gives you a wide, sweeping, faintly rainbow effect, which suits a hero and rarely suits a button. But it is a genuine third option alongside "pick different colors" and "add a middle stop," and most gradient advice never mentions it exists.

How do you move a gradient's midpoint without adding a color?

With a bare percentage between the two stops. It's the least known piece of gradient syntax and it solves a problem people usually reach for a third color to fix.

Here's the situation. Your two colors are right, the interpolation space is right, but the blend turns over too early or too late. The light half feels like it's swallowing the dark half. The instinct is to add a middle stop, which works but costs you a third color to choose and maintain, and gives the gradient another place to go wrong.

The W3C's CSS Images Module Level 3 gives you a lighter tool, called a transition hint. The spec describes it as a position between two color stops representing the halfway point in the color transition. Write a length or percentage on its own, with no color attached, and the interpolation stops being linear. What you're setting is where the halfway color, meaning the 50 percent blend of the two surrounding stops, actually lands.

background: linear-gradient(to right, #4338ca, #22d3ee); background: linear-gradient(in oklab to right, #4338ca, 30%, #22d3ee);

That 30% is the hint. No third color, no new value in your palette. The blend now reaches its midpoint a third of the way across, so the cyan end gets more room and the indigo holds its identity for less of the run.

One detail from the spec that tells you the syntax is safe to reach for. When the hint sits exactly halfway between the two surrounding stops, the algorithm produces ordinary linear interpolation. So a hint at 50 percent changes nothing, which means you can add one, nudge it, and know that the neutral position is the behaviour you already had.

Two things worth pairing this with. A hint is about where the blend turns, and the interpolation space is about what colors it turns through, so they're independent and you'll usually want both. And a hint is often the honest fix for the complementary-pair problem further up, because pushing the midpoint away from centre shortens the time your blend spends in the dead zone even when it still has to pass through it.

What happens when you fade a gradient to transparent?

Something better than you'd expect, and the reason is worth knowing because it's the one place gradients have a sensible default already built in.

The trap in principle: transparent isn't a neutral absence of color. It's transparent black. So a naive blend from a bright orange to transparent should drag the whole run toward grey on its way out, giving you a dirty smear instead of a clean fade. Plenty of design tools do exactly that.

CSS doesn't, because the spec requires premultiplied alpha. A premultiplied color, in the spec's words, has the alpha channel multiplied into the color channels rather than being processed independently, and it states that interpolating with premultiplied representations tends to produce more attractive transitions, particularly when moving from a fully opaque color to fully transparent. So your orange fades out as orange.

The precise condition is the useful bit. The spec notes that transitions holding either the color or the transparency constant give identical results either way, and that differences only arise when both the color and the transparency differ between the two ends. Fading a color to transparent changes both at once, since the color is going to black and the alpha to zero, so it's exactly the case where this matters.

What to do with that. Fading to transparent in CSS is fine, and it's the right way to build the photo overlay and scrim described later on this page. But if you're recreating the same gradient in a design tool and it comes out muddier than the browser, this is usually why, and the fix is to fade to your own color at zero alpha rather than to a generic transparent.

How do you build a gradient from one brand color?

Derive the second stop from the first rather than picking it by eye. Earlier I said gradients should come out of your palette rather than replace it, and CSS now lets you do that literally, in the stylesheet, with the brand color as the only value you hard-code.

Two functions from the W3C's CSS Color Module Level 5 do the work.

Relative color syntax, for a stop that tracks your brand

The spec lets you open a color function with an origin color, described as a from <color> value at the start of the function. Inside, you refer to the origin's channels by keyword and adjust only what you want. In oklch() those keywords are l, c, h and alpha.

The spec's own example takes gold and softens its chroma while leaving lightness and hue untouched:

--base: gold; --softerbase: oklch(from var(--base) l calc(c * 0.9) h);

That is exactly the move you want for a gradient. Keep the hue, nudge one channel, and the second stop is guaranteed to be related to the first rather than merely near it. Change the brand color later and both ends follow.

The same pattern gives you the other useful derivations. A hue rotation for a complement, straight from the spec:

--accent: lightseagreen; --complement: lch(from var(--accent) l c calc(h + 180));

And a greyscale version, by zeroing the two chroma channels in Lab:

--mygray: lab(from var(--mycolor) l 0 0);

Two details from the spec that will bite you otherwise. If you leave alpha out, it inherits the origin color's alpha rather than defaulting to fully opaque. And the color components are not clamped, so a calc() that pushes chroma past what the space can show will not error, it will just resolve somewhere you did not intend. Check the result rather than trusting the arithmetic.

color-mix(), and the default nobody tells you about

The other function blends two colors into one, which is how you make a middle stop that genuinely sits between your two ends rather than being guessed.

Here is the detail worth the whole section. The spec states that if no color interpolation method is specified, color-mix() assumes Oklab. Gradients do the opposite and default to sRGB, which is the entire cause of the muddy-middle problem further up this page. So the two features you would assume behave the same way have opposite defaults.

What that means practically. A midpoint you generate with color-mix() is already computed in a perceptually uniform space, for free, with no keyword. But dropping that midpoint into a gradient does nothing to fix the gradient itself, because the blend either side of it still runs through sRGB unless you say otherwise. You still need in oklab on the gradient. One good default does not rescue the other.

So the honest recipe is both:

--brand: #4338ca; --soft: oklch(from var(--brand) calc(l + 0.12) calc(c * 0.85) h); background: linear-gradient(to right, var(--brand), var(--soft)); background: linear-gradient(in oklab to right, var(--brand), var(--soft));

Note the two background lines again. Same fallback discipline as everywhere else on this page, and it matters more here, because a browser that cannot parse relative color syntax will drop the custom property too and you can end up with nothing rather than something duller.

One judgment call this does not make for you. Deriving a stop mathematically guarantees the two colors are related. It does not guarantee they look good together, and a small nudge in chroma can be too subtle to read as a gradient at all. Generate it, then look at it at the actual size it will appear. Our gradient generator is the quicker way to eyeball that, and the brand color guide covers choosing the base color these all derive from.

How do you keep text readable on a gradient?

Measure contrast at the worst point, not the average. That is the whole discipline.

A gradient running from light to dark will pass a contrast check at one end and fail at the other. Designers test the middle, see a comfortable number, and ship something that becomes unreadable in the corner. The failure is invisible to you because you know what the text says.

WCAG 2.2 success criterion 1.4.3 sets the bar at Level AA: at least 4.5 to 1 for normal text, and at least 3 to 1 for large text, where large means 18 point or 14 point bold, roughly 24px and 18.5px. The criterion also lists insufficient contrast caused by a background image as a failure, and a gradient background behaves the same way.

Three ways to handle it:

Check both ends with a real tool rather than by eye. Our contrast checker gives you the ratio, and the contrast accessibility guide explains what the numbers mean.

Can you make the text itself a gradient?

You can, and it is one line more complicated than people expect. The section above is about text sitting on a gradient. This is the other thing: letterforms filled with the gradient. You paint the gradient as a background, clip it to the glyphs, then make the text's own fill transparent so the background shows through.

.headline {
  background: linear-gradient(in oklab to right, #7c3aed, #06b6d4);
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

Keep the -webkit-text-fill-color line even though color: transparent looks like it should be enough. It is the more reliable way to knock out the fill across engines, and it is the property that actually wins when both are set.

Now the failure mode, because this technique has a nasty one. You have made your text transparent. If anything stops that gradient painting, the letters do not fall back to a readable color, they render as nothing at all. MDN says this directly in its guidance on background-clip: if the background does not load, the text can become unreadable, so set a fallback background color and test without the image. The same logic applies to the gradient. Invisible text is a much worse outcome than an unstyled headline.

The place this actually bites is forced colors mode, which the guide covers further down. Forced colors reverts background-image gradients to the user's chosen colors. If your transparent text fill survives while the gradient behind it does not, the headline vanishes for exactly the people who turned on a high contrast theme to be able to read it. Put the fill back explicitly.

@media (forced-colors: active) {
  .headline {
    background: none;
    -webkit-text-fill-color: revert;
    color: CanvasText;
  }
}

Contrast needs a different habit here too. You cannot test a gradient headline as one color, and testing the average is the mistake that passes a check while failing a reader. Test the lightest stop against your background, because that is the weakest point in the run, and if that passes the rest of the gradient does as well.

One genuine advantage over the alternative. People used to ship headlines like this as exported images, which meant the text was not text: unselectable, unsearchable, and blurry when someone zoomed. A gradient headline is still real text, so screen readers announce it normally, it reflows, it scales, and it survives translation. That is a real gain and worth keeping.

Be sparing about where you use it. Gradient fill costs you contrast at the extremes of the run, and body copy has no margin to spend on that. Use it on a headline, a logotype or a single number you want people to look at, keep everything you actually need read in a flat color, and the technique earns its place.

Do gradient buttons pass accessibility checks?

They can, but there is a second criterion people forget about.

Text on the button is covered by 1.4.3 above. The button itself is covered by WCAG 2.2 success criterion 1.4.11, which requires at least 3 to 1 for user interface components and meaningful graphical objects, also at Level AA.

The detail that matters for gradients is how it is measured. The criterion says contrast should be assessed against adjacent colors at the point where the element appears, not across the whole page. So a gradient button sitting on a gradient background has to clear 3 to 1 at every point along its edge, and the two gradients are both moving. That is a genuinely hard thing to get right, and it is a good argument for putting a gradient button on a flat background.

Focus states need attention too. If your focus ring is a colour that works against the light end of the gradient and vanishes against the dark end, keyboard users lose track of where they are.

Is the contrast ratio the whole story?

No, and gradients are where that shows up most. The ratio is still the number you have to hit, so keep hitting it. But it is worth knowing what it does and does not model, because a gradient can pass the check and still read badly.

The WCAG 2.x ratio is computed from relative luminance, and it treats a pair of colors the same way regardless of which one is the text. Perceived contrast does not actually behave like that. Light text on a dark background and dark text on a light background of the same measured ratio do not read as equally clear, and the formula is known to be least reliable at the dark end of the range. On a gradient that runs from light to dark, you are moving through exactly the region where a single number is doing the most guessing.

The standards work reflects this. The visual contrast guidance being developed for WCAG 3 uses a different method, the Advanced Perceptual Contrast Algorithm, which is polarity-aware and feeds into a minimum font size and weight rather than one pass-or-fail figure. That is a better fit for how text on a shifting background actually behaves.

Be careful how much weight you put on it though. W3C says WCAG 3 "is currently an incomplete draft", is "not expected to be a completed W3C standard for a few more years", and that its final requirements will differ from the current draft. It also confirms WCAG 3 "will not supersede WCAG 2 and WCAG 2 will not be deprecated for at least several years after WCAG 3 is finalized".

So the practical position for now is unchanged. Meet 4.5 to 1 and 3 to 1 against the worst point of your gradient, because that is what you are measured against and what an auditor will check. Treat the perceptual side as a tiebreaker: if a heading technically passes but still looks thin against the busy part of the blend, believe your eyes and fix it rather than pointing at the number. Passing is the floor, not the goal.

Do gradients work for people with color blindness?

Decorative ones, mostly fine. Gradients that carry meaning, often not, and this is the failure mode that contrast ratios will never catch for you.

Draw the line first, because it decides everything else. A gradient behind your hero image is decoration. Nobody has to decode it. But the moment a gradient encodes a value, a heatmap, a status bar running red to green, a chart legend, a load indicator, it stops being decoration and becomes information. If a reader can't tell the colors apart, they can't read the data, and no amount of AA-compliant text on top fixes that.

The numbers come from Crameri, Shephard and Heron, writing in Nature Communications in 2020. The general worldwide estimate they cite is that 0.5 per cent of women and 8 per cent of men have a color-vision deficiency. Eight per cent of men is not an edge case. On a page with a thousand male readers, eighty of them are getting a different picture than the one you designed.

They also flag something most accessibility writing skips. Those rates aren't uniform. CVD is lower, and nearly disappears, in populations from sub-Saharan Africa, and likely significantly higher in populations with a larger fraction of white people, Scandinavia being their example. So the global average is a starting point, not a number to plan a specific audience around.

What makes a gradient unreadable?

Two things, and they're separate problems that people tend to lump together.

The first is red paired with green at similar lightness. The paper is blunt that color maps combining both cannot be read by a large fraction of the readership. If the only thing separating your "good" state from your "bad" state is hue, and the two sit at the same lightness, a red-green deficient reader sees one continuous smear. Vary lightness as well as hue and the problem largely goes away, because lightness survives every form of CVD.

The second is uneven perceptual spacing, which affects everybody. Rainbow-style maps like jet aren't perceptually uniform, so equal steps in your data don't produce equal steps in what the eye sees. Crameri and colleagues put a figure on the damage: visual error can exceed 7 per cent of the displayed data variation, enough that a linear graph such as a flat line becomes unrecognisable. Part of the cause is that yellow is the brightest color in a rainbow ramp and pulls the eye hardest, even though its position carries no special meaning. You get a bright band drawing attention to an arbitrary point in the middle of your scale.

Which loops back to the interpolation fix earlier on this page. Blending through Oklab or Oklch isn't only about keeping the midpoint from going muddy. Perceptually uniform spaces weight the same data variation equally across the whole range, so the smoothness you gain is also accuracy. One change buys you both.

Practical version, three rules. Never encode meaning in hue alone, always move lightness too. Avoid red-to-green ramps for anything with a value attached, and pair the color with a label, an icon or a pattern where it genuinely matters. And test in greyscale, because a gradient that still reads with the color stripped out will read for nearly everyone. Our guides to color accessibility and WCAG and accessible color design go further on both.

How should a gradient change in dark mode?

Rebuild it, don't invert it. A gradient tuned against a white page almost always looks wrong against a dark one, and flipping the stops makes it worse rather than better.

The mechanism is simple once you see it. On white, a mid-saturation gradient reads as a tint sitting slightly behind the content. Put the same colors on a near-black surface and they stop sitting behind anything. They glow. Saturated hues against dark backgrounds appear to advance and bloom at the edges, so a gradient that was a quiet background in light mode becomes the loudest thing on the screen in dark mode, competing with the text it was supposed to sit under.

Three adjustments fix most of it:

Wire it up with the prefers-color-scheme media feature, defined in the W3C Media Queries Level 5 spec as a user preference feature that reports whether the person wants a light or dark scheme. It reflects an actual system setting rather than a device capability, so treat it as a real statement of intent and define both gradients properly instead of letting one degrade into the other.

Then check the text again. Your contrast measurement from the light-mode gradient does not carry over, because both the background and usually the text color have changed. Re-measure at the worst point of the new gradient, same as before. Our dark mode color guide covers the wider palette work this sits inside.

What happens to your gradient in forced colors mode?

It disappears. Completely. This surprises most designers, and it's the one gradient failure mode that no amount of contrast tuning will save you from.

Forced colors mode is the accessibility feature behind Windows High Contrast and similar settings. The W3C CSS Color Adjustment Module Level 1 defines it as the user agent enforcing the user's preferred color palette over the author's chosen colors. Buried in that spec is the line that matters here: background-image computes to none unless it contains a url() function. A CSS gradient is a background-image with no url() in it. So your gradient is not dimmed or approximated. It is removed, and the element falls back to a system background color.

The same pass sets box-shadow and text-shadow to none, which takes out the other two things designers often lean on for depth.

What this actually breaks:

You can test and adapt with the forced-colors media query, and the spec provides forced-color-adjust with values of auto, none, and preserve-parent-color for the rare cases where you genuinely need to keep your own colors. Reach for none sparingly. Overriding a setting someone turned on deliberately for accessibility reasons is usually the wrong instinct, and the better fix is designing so the gradient was decoration all along.

How do you make a gradient repeat or tile?

Two different jobs, and people mix them up constantly. One is a gradient that repeats its own color ramp over and over, which is how you get stripes. The other is a gradient used as a tile that fills a box with a pattern. They need different properties, and the second one fails silently if you skip a step.

Start with the ramp. The repeating-linear-gradient() and repeating-radial-gradient() functions take exactly the same syntax as the ordinary versions. The difference is what happens after the last stop. CSS Images Module Level 3 defines it precisely: the color stops repeat infinitely in both directions, shifted by multiples of the distance between the first stop and the last one.

So repeating-linear-gradient(red 10px, blue 50px) behaves like a normal gradient running red at 10px, blue at 50px, red at 50px, blue at 90px, and onward forever in both directions. A 40px cycle, repeated.

Why does your repeating gradient have a hard line in it?

Because your first and last colors don't match, and the spec is blunt about what that produces.

The last stop and the first stop always land on top of each other at every cycle boundary. CSS Images 3 states that this "will produce sharp transitions if the gradient does not start and end with the same color." Red to blue gives you a smooth red-to-blue sweep, then an instant snap back to red, forty pixels later, again and again.

Sometimes that's exactly what you want. Barber poles, hazard tape and progress stripes all rely on that hard edge. But if you were after a soft repeating wash, the fix is to close the loop: end on the color you opened with, so red, blue, red rather than red, blue. You spend one extra stop and the seam disappears.

Why won't your gradient tile?

Because it's already filling the whole box, so there's nothing left to repeat.

This is the part that catches people. A gradient is an image, but unlike a photo it has no natural size. CSS Backgrounds and Borders Module Level 3 handles that case under background-size: auto by sizing an image with no natural dimensions the way contain would, which in practice means it stretches to cover the background area. Setting background-repeat: repeat then does nothing visible, because one tile is already the size of the element.

Give it a size and it starts behaving like a pattern. Once you set something like background-size: 40px 40px, the gradient becomes a 40 by 40 tile and repeat has something to work with. That's the whole trick, and it's one line.

There's a tidier option for fixed-size containers too. background-repeat: round rescales the tile so a whole number of them fits the space, using the ratio the spec sets out, rather than clipping the last one halfway through. Useful when a chopped-off final stripe would look like a bug.

One word of restraint, since it applies here more than anywhere else on this page. Repeating gradients are cheap to write and very easy to overdo. A subtle hatch behind a card reads as texture. The same pattern at full contrast across a hero reads as a test image. And a busy repeating background is one of the fastest ways to wreck the text contrast covered further up, because your foreground now sits on two different colors instead of one.

What causes gradient banding and how do you fix it?

Banding is the visible stepping you sometimes see across a large gradient, where a smooth blend turns into a set of stripes. It happens when the gradient covers a wide area with very few color values available to describe the change, so the display rounds neighbouring pixels to the same value and you see the joins.

It shows up most on large hero backgrounds, on subtle gradients between two very similar colors, and on darker gradients where there is less headroom to work with.

What helps:

What happens to your gradient on a wide-gamut screen?

It can look like a different gradient, and if you're writing stops in oklch you may already be asking for colors the screen cannot produce. Design a ramp on a recent laptop, then open it on an older monitor, and the saturated end can quietly flatten out.

The size of the gap is bigger than most people assume. CSS Color 4 publishes a table of gamut volumes for the predefined color spaces, measured in millions of Lab units. sRGB comes in at 0.820. display-p3 is 1.233, so roughly 150 percent of sRGB. rec2020 reaches 2.042, about 249 percent. Those aren't rounding differences. That's a large amount of color that simply doesn't exist on an sRGB panel.

So what happens to the colors that don't fit? The spec is direct about it. An out-of-gamut color gets gamut mapped for display, producing what it describes as a similar-looking but lower chroma, meaning less saturated, color.

Here's why that matters more for gradients than for flat fills. A solid color that maps down is just slightly duller, and nobody notices. A gradient is a whole ramp of colors, and the mapping doesn't apply evenly along it. The stops that sat furthest outside sRGB get pulled in hardest while the ones already inside barely move. Your carefully spaced ramp compresses unevenly, and it tends to go slack exactly where you put the most saturated part.

This is also the sneaky failure mode of authoring in oklch. It's easy to write a perfectly valid oklch stop with more chroma than sRGB can hold, because the syntax doesn't stop you. It looks superb on the P3 screen you designed it on, and it's a duller, shorter ramp for a chunk of your audience.

The tool for handling this properly is the color-gamut media feature in Media Queries 5, which takes srgb, p3 or rec2020 and tells you what the display can actually manage. Write the sRGB-safe gradient as your base, then enhance inside a p3 query.

Working rules:

And this compounds with the muddy-middle problem further up the page. Interpolation space decides the path your gradient takes between stops. Gamut mapping decides whether the end of that path can be shown at all. Get one right and ignore the other and you can still end up with a ramp that looks dull on half the machines that load it.

Do animated gradients need special handling?

Yes, and this is the one place where a gradient can fail a stricter accessibility bar than anything else on this page.

The slowly shifting gradient hero is everywhere right now. It usually looks good. But the moment it animates on its own, it stops being a background and becomes moving content, and a different rule applies.

WCAG 2.2 success criterion 2.2.2, Pause Stop Hide, covers moving, blinking or scrolling content that starts automatically, lasts more than five seconds, and sits in parallel with other content. Where all three are true, you have to give the user a way to pause, stop or hide it, unless the movement is essential to what the thing is.

Notice the conformance level. Everything else in this article is Level AA. This one is Level A, the minimum bar. So a designer can obsess over 4.5 to 1 contrast, get it perfect, and still be failing a stricter criterion because the background quietly loops forever behind the text.

Read the three conditions carefully, though, because they are cumulative and a lot of gradient animations do not meet all of them. A five-second intro that settles and stops is fine. So is a gradient that only moves on hover, since that is not automatic. What catches you is the infinite loop, which is exactly the one most people build.

The one-line fix most animated gradients are missing

Respect the operating system setting. Both macOS and Windows let people turn motion down system-wide, usually because of vestibular disorders where drifting movement causes real nausea and dizziness. The browser exposes that choice, and honouring it takes a few lines:

@media (prefers-reduced-motion: reduce) { .gradient-hero { animation: none; } }

That is not a fallback for old browsers. It is a person telling you they get motion sick, and a query that lets you hear it. Build the animation, then switch it off for the people who asked you to.

Two practical notes. Keep the static end state looking finished rather than half-rendered, since that is what those users will see. And if your gradient animates by shifting hue rather than position, the arc keywords from earlier still apply, so an animation running the long way round the wheel will sweep through far more colour than you probably intended.

Why will your gradient colors not animate at all?

Because a CSS custom property has no type until you give it one, and an untyped value cannot be interpolated. This is the wall nearly everyone hits the first time they try to animate a gradient, and it is why so many animated gradient tutorials quietly slide background-position across an oversized gradient instead of animating the colors themselves.

Put a color in a custom property, animate it from one value to another, and it snaps instead of blending. The browser is not being awkward. The W3C's CSS Properties and Values API Level 1 says custom property values "interpolate by computed value, in accordance with the type that they parsed as", and an unregistered property has not parsed as any type at all. It is a bag of tokens. There is nothing in it to interpolate between.

Registering the property is the fix. The @property rule hands the browser the type information it was missing:

@property --stop-one { syntax: "<color>"; inherits: false; initial-value: oklch(70% 0.15 250); }

Two details catch people out. syntax and inherits are both required, and initial-value is required too for anything other than syntax: "*". That initial value also has to be computationally independent, so 200px is allowed and 3em is not, because em depends on a parent font size the browser cannot resolve at registration time.

Support reached Baseline in 2024, newly available from July that year, so it is safe for most audiences now and still worth a static fallback if you support older devices. And it changes nothing about the rules above. Registering a property makes the animation possible, not exempt. The reduced-motion query and the Pause Stop Hide criterion both still apply to it.

Are gradients expensive to render?

A static one, no. Paint it once and the browser is done. An animated one can be genuinely costly, and the whole difference comes down to which property you put the animation on.

Start from what a gradient actually is. Every gradient function is an image value, which means it lands on background-image rather than on a colour property. That matters because of how browsers split rendering work. MDN sorts animated properties into three tiers by cost:

Animating the gradient itself, meaning the stops or the angle, sits in that middle tier at best. The browser has to redraw the image every frame, and on a large hero section that is a lot of pixels sixty times a second. It is the classic reason a page feels fine on your laptop and stutters on a three-year-old phone.

The fix is to keep the gradient still and move something else.

Two things not to over-think. A static gradient behind a card or a button is not a performance problem and never was, so do not go replacing them with flat colours on a hunch. And grain overlays, covered further up, are a paint cost only when they move. A fixed noise texture is painted once like anything else.

Whatever you animate, the reduced-motion rule above still applies. The cheapest animation is the one you do not run for somebody who asked you not to.

Which gradient styles look current and which look dated?

Three styles read as current: soft two-stop blends that behave like light rather than paint, mesh gradients with several colors bleeding into each other, and grainy gradients with noise laid over the top. The dated ones are corner-to-corner rainbows, hard diagonal splits, and anything with a glossy highlight bar across it.

The common thread in the current styles is restraint plus texture. A soft blend keeps the two colors close on the wheel and the contrast low, so the gradient adds dimension without announcing itself. That is the opposite of the 2010s approach, where the gradient was the decoration. If you want the broader palette picture, our web design color trends guide covers where gradients sit alongside everything else.

Mesh gradients are the one people ask about most, and there is no CSS primitive for them. A real mesh gradient blends multiple color points across a two-dimensional surface, which is a Photoshop or Illustrator operation. In the browser you fake it, and the fake is convincing. Stack three or four radial-gradient() layers in a single background declaration, put each one at a different position with at 20% 30% style syntax, and let each fade to transparent so the layers below show through. The overlaps do the blending for you.

The other route is conic-gradient(), defined in the W3C CSS Images Module Level 4 alongside the linear and radial functions. A conic gradient sweeps color around a center point rather than along a line, and a heavily blurred one reads as an organic blob rather than a pinwheel. Both approaches accept the same in oklab interpolation fix from earlier in this guide, so use it. Mesh gradients have more color pairs meeting each other than a two-stop gradient does, which means more chances for a grey dead zone.

Grainy gradients solve a real problem rather than just looking fashionable. Noise breaks up the flat bands that appear when a gradient spreads a small color difference across a lot of pixels, which is the banding issue covered above. The browser-native way to generate that noise is SVG's feTurbulence filter primitive, specified in the W3C Filter Effects Module Level 1. Two attributes do the work: baseFrequency sets the grain size, and numOctaves sets how many layers of detail get summed. The spec also defines exactly how the noise becomes color, converting the turbulence result with colorValue = (turbFunctionResult * 255) and clamping to the range 0 to 255.

In practice you generate one small noise tile, encode it as a data URI, and overlay it at 3 to 8 percent opacity. Keep it subtle. Grain you can consciously see is grain that has gone too far, and a full-page feTurbulence filter applied live is genuinely expensive to render, so bake it into an image rather than filtering a large element on every paint.

Which gradient mistakes make a design look amateur?

Five, in rough order of how often they show up.

Too many colors. Two is right for nearly everything. Three when you need a shift the eye can follow across a long space. Four or more and you have made a rainbow, and rainbows are very hard to use without looking like a free template.

Complementary pairs blended straight through the middle. Covered above. The grey dead zone is the giveaway.

Gradients on small elements. On a 24px icon or a thin border, the shift is too small to perceive. You have added complexity nobody can see.

Gradients behind body text. Even when contrast technically passes, a shifting background makes sustained reading harder. Save gradients for headings, heroes and decorative areas.

Competing gradients. One per screen. Two gradients fighting for attention read as busy rather than rich, and neither gets the emphasis you were after.

If you are working in dark mode, note that gradients behave differently there. Dark backgrounds have less room before banding appears, and a gradient that looked rich on white can go muddy on near-black. Our dark mode color guide covers the adjustments.

What else do people ask about gradient design?

Why does my gradient look muddy or grey in the middle?

Because it is blending through sRGB by default, and the straight line between two saturated colors in that space passes through a desaturated zone. The W3C CSS Images Module Level 4 notes that choosing a polar rather than rectangular colorspace avoids this, since spaces like LCH and Oklch are inherently chroma-preserving. Adding an interpolation method such as in oklab to your gradient usually fixes it in one line.

How many colors should a gradient have?

Two for almost everything. Three when you genuinely need a shift the eye can follow, such as a long hero background. Beyond three you are usually decorating rather than designing, and every extra stop adds another place the gradient can go muddy or clash with text sitting on top of it.

Can you put text on top of a gradient?

Yes, but you have to check contrast at the worst point of the gradient, not the average. WCAG 2.2 success criterion 1.4.3 requires at least 4.5 to 1 for normal text and 3 to 1 for large text at Level AA, where large means 18 point or 14 point bold. Insufficient contrast caused by a background image is a listed failure, and a gradient behaves the same way.

What contrast do gradient buttons need?

WCAG 2.2 success criterion 1.4.11 requires at least 3 to 1 for user interface components and meaningful graphical objects, at Level AA. The criterion says contrast should be measured against adjacent colors at the point where the element appears, not across the whole page, which matters when your button sits on a background that is changing colour behind it.

When should you not use a gradient?

Behind dense body text, on small UI elements where the shift is invisible anyway, and anywhere you have already used one nearby. Gradients work by drawing the eye, so two competing gradients on one screen cancel each other out. Flat color is the safer default and a gradient should be a deliberate exception.

Build and preview a gradient in your browser, free and with no signup. Open the gradient generator →

Which sources is this based on?

Specifications are living documents, so check the current text before relying on a detail.