How to make a gradient in CSS
The three functions and their full syntax, the reason a two-colour blend goes grey in the middle, and the four jobs people actually want a gradient for.
Free toolCSS Gradient GeneratorLinear, radial and conic gradients with per-stop control. Copy the CSS, or save the same gradient as a PNG.Open the generatorThe short answer
Set background-image to one of three functions and give it a list of colours. That is the whole idea.
background-image: linear-gradient(135deg, #3b2f78, #f6a45c);Everything else in this article is either a way of controlling that line more precisely, or a way of pointing the result at something other than a background.
One thing to fix straight away: it goes on background-image, not background-color. A gradient is an image as far as CSS is concerned. The shorthand background accepts it because the shorthand accepts an image, but it also resets every other background property to its initial value, which is how a gradient sometimes wipes out a background-position set three lines above it.
Linear gradients
linear-gradient(to right, #0b2b3c, #1f7a8c)
linear-gradient(160deg, #0b2b3c, #1f7a8c)
linear-gradient(to bottom right, #0b2b3c, #1f7a8c)The direction comes first and is optional. It can be an angle or a keyword. An angle of 0deg points straight up and turns clockwise, so 90deg runs left to right. Leave it out and you get to bottom.
The keyword form is worth knowing because to bottom right is not the same as 45deg on anything that is not square. The corner keywords guarantee the gradient line ends at that corner whatever the box's shape, so a card that changes width keeps the same picture. A fixed angle does not.
Radial gradients
radial-gradient(#fff3d6, #160f2e)
radial-gradient(circle at 30% 25%, #fff3d6, #160f2e)
radial-gradient(circle closest-side at 30% 25%, #fff3d6, #160f2e)Three things can be set, all optional: the shape, the size and the position. Shape is circle or ellipse, and the default is an ellipse matching the box. Position works like background-position.
Size is the one people miss, and it decides where the last colour lands.
| Size | The 100% stop lands at |
|---|---|
| closest-side | The nearest edge of the box. |
| closest-corner | The nearest corner. |
| farthest-side | The furthest edge. |
| farthest-corner | The furthest corner. This is the default. |
For a soft glow behind a card, closest-side is usually what you meant: the default runs all the way to the far corner, so the interesting part of the fade is off the edge of the box and what you see is the boring middle of it.
Conic gradients
conic-gradient(#e94f6d, #5a4be0, #2bb6a4, #e94f6d)
conic-gradient(from 210deg at 50% 50%, #e94f6d, #5a4be0)The sweep starts at twelve o'clock and turns clockwise. from rotates the start, and at moves the centre. Repeating the first colour as the last one is what makes a smooth wheel: without it there is a hard seam where the end meets the start.
Conic gradients are how you draw a pie chart in one declaration, using hard stops:
conic-gradient(#3b2f78 0 40%, #b8496b 40% 75%, #f6a45c 75% 100%)Colour stops, and the two things they can do
A stop is a colour and optionally a position. Give a stop two positions and it covers that whole range as a flat band, which is where hard edges come from. Give two stops the same position and the change between them is instant.
/* a fade */
linear-gradient(90deg, #222 0%, #eee 100%)
/* a hard split down the middle */
linear-gradient(90deg, #222 0 50%, #eee 50% 100%)
/* stripes, tiled forever */
repeating-linear-gradient(45deg, #222 0 10px, #333 10px 20px)There is also a form with no colour at all: a bare percentage between two stops is an interpolation hint, and it moves the midpoint of the blend without adding a third colour. linear-gradient(red, 30%, blue) puts the halfway colour at 30% along, so the fade starts fast and finishes slowly. It is the diamond handle from every design tool, and almost nobody knows CSS has it.
The grey middle, and how to fix it
Blend blue into yellow and the middle is a muddy grey. This is the complaint behind most searches for a gradient tool, and it is not a bug.
By default CSS interpolates in sRGB, which stores colour the way a display emits light rather than the way an eye reads it. The straight line between two distant hues in that space passes close to the point where they cancel. Since 2023 you can name a different space instead:
linear-gradient(in oklch, blue, yellow)
linear-gradient(90deg in oklab, blue, yellow)
linear-gradient(in hsl longer hue, blue, yellow)| Space | What it does | Reach for it when |
|---|---|---|
| srgb | The default. Fast, and grey through the middle of a wide blend. | The two colours are close together, or you have to support an old browser. |
| oklab | Perceptually even. No hue sweep, so a straight blend. | You want the shortest honest path between two colours. |
| oklch | Perceptually even, and travels around the hue circle. | You want the rainbow path. Blue to yellow through green. |
| hsl | Sweeps hue, but the lightness is not perceptual. | You want the old colour-wheel look on purpose. |
In a space with a hue channel you can also pick which way round the circle to go, with shorter hue (the default), longer hue, increasing hue or decreasing hue. longer hue between two nearly identical colours walks through every hue there is, which is the one-line rainbow.
Support: Chrome and Edge 111, Safari 16.2, Firefox 128. An older browser drops the whole declaration rather than falling back, so if you still support one, write a plain sRGB gradient first and let this override it.
The fix that works everywhere is older and duller: add a third stop in the middle and choose its colour by hand.
Gradient text
.headline {
background-image: linear-gradient(135deg, #3b2f78, #f6a45c);
background-clip: text;
color: transparent;
}Three declarations, and they only work together. The gradient paints the element's background, background-clip: text cuts that background to the shape of the glyphs, and color: transparent stops the text painting over the top of it.
Two warnings. If the CSS fails to load or the property is unsupported, the text is invisible rather than plain, so add @supports (background-clip: text) around it if the headline matters. And an automated contrast check will pass this and mean nothing, because the text has become an image. Judge it by eye, and keep it off body copy.
Gradient borders
border-image is the obvious answer and it has one fatal limitation: it ignores border-radius. If the corners are square it is one line. If they are not, stack two backgrounds instead and let the padding box cover the middle of the outer one:
.card {
border: 2px solid transparent;
border-radius: 12px;
background:
linear-gradient(var(--surface), var(--surface)) padding-box,
linear-gradient(135deg, #3b2f78, #f6a45c) border-box;
}The first gradient is a flat fill of the surface colour clipped to the padding box, and it hides everything except the two-pixel ring the border box leaves showing. The transparent border is what reserves that ring. It works with any radius.
Animating a gradient
background-image is not animatable, so a transition between two gradients snaps. Three approaches work, in increasing order of how well they perform.
Register the colours. A custom property declared with @property and a syntax of <color> becomes an animatable value, so the gradient can be rebuilt from properties that transition:
@property --grad-a {
syntax: '<color>';
initial-value: #3b2f78;
inherits: false;
}
.hero {
--grad-a: #3b2f78;
background-image: linear-gradient(135deg, var(--grad-a), #f6a45c);
transition: --grad-a 600ms ease;
}
.hero:hover { --grad-a: #b8496b; }Move a bigger background. Make the gradient two or three times the size of its box and animate background-position. This is the slow drifting hero background you have seen everywhere. It repaints on every frame, so keep it off anything that scrolls.
Cross-fade two layers. Put each gradient on a pseudo-element and animate opacity between them. Opacity is composited on the GPU, so this is the only one of the three that costs nothing per frame. It is also the only one that can move between two gradients of different types.
Where else gradients come from
| Tool | Good for | Watch out for |
|---|---|---|
| Browser tool | Getting the CSS, and a matching PNG. | No mesh gradients. CSS has no such function. |
| Figma | Designing beside the rest of the interface. | Angles are measured differently, so copied CSS often arrives rotated. |
| Photoshop | Mesh and noise, and gradient maps over a photo. | The output is pixels. There is no CSS to take away. |
| Tailwind | Staying inside the utility classes. | Its own scale covers two or three stops. Past that you need an arbitrary value. |
| SVG | Gradients on strokes and on shapes, not just boxes. | More markup, and a second syntax to remember. |
Tailwind's built-in form is bg-gradient-to-r from-indigo-900 via-rose-600 to-orange-300, which covers three stops. For anything more specific, an arbitrary value takes the whole function with underscores standing in for spaces.
Three things worth checking before you ship one
- Look at it full width. Banding shows up over distance. A gradient that is smooth in a 400px card can have four visible steps across a 1600px hero.
- Check the text on top of it. Contrast has to hold at the worst point of the gradient, not the average one. That is usually one corner.
- Try it in both themes. A gradient built against a white page often disappears against a dark one, because it was carrying the contrast the background used to provide.