I lost most of an afternoon to a gradient once. Not a complicated one โ a hero background on a WP Adminify marketing page, brand blue fading into brand orange, diagonal. I wrote linear-gradient(45deg, #4466EE, #E99537) and it went the wrong way. So I tried -45deg. Also wrong. Then 135deg, which was right, and I moved on without understanding why, which is the worst possible outcome because it guarantees you'll get it wrong again.
The reason is that CSS gradient angles are not the angles you learned in maths class. 0deg points up, and the angle increases clockwise. In trigonometry โ and in SVG, and in canvas โ 0 points right and angles increase counter-clockwise. Two different conventions, one identical-looking deg unit, and no error message when you pick the wrong one. It's written down plainly in CSS Images Module Level 3, and I had simply never read it.
That's the case for a visual generator in one anecdote. The Gradient Generator on toolz.dev gives you a live preview, draggable colour stops, and copyable CSS for linear, radial, and conic gradients โ so the angle question answers itself instead of costing you an afternoon.
TL;DR: In CSS,
0degpoints to the top and angles run clockwise, soto rightequals90degand a top-left โ bottom-right diagonal is135deg, not45deg. Use Gradient Generator to see it rather than guess it, and the Color Picker to pick stops that don't go muddy in the middle. Gradients are<image>values โ anywhere you can useurl(), you can use a gradient. All client-side, no signup.
What Is a CSS Gradient, Technically?
A gradient is not a colour. It's an image. That single fact explains most of the syntax.
Per CSS Images Level 3, linear-gradient(), radial-gradient(), and their repeating variants produce a value of type <image>. That means anywhere the spec accepts url('photo.jpg'), it also accepts a gradient: background-image, border-image, list-style-image, mask-image, content. It also means gradients have no intrinsic dimensions โ they stretch to fill the box you put them in, which is why the same gradient looks different on a 200px button and a 1400px hero.
conic-gradient() came later and lives in CSS Images Level 4, alongside the interpolation-colour-space syntax I'll get to below.
What's the Difference Between Linear, Radial, and Conic?
| Linear | Radial | Conic | |
|---|---|---|---|
| Colour travels | Along a straight line | Outward from a centre point | Around a centre point |
| Defined by | An angle or to <side> |
Shape (circle/ellipse), size, position |
Start angle, position |
| Reach for it when | Backgrounds, buttons, overlays, image scrims | Spotlights, glows, vignettes, mesh backgrounds | Pie charts, colour wheels, progress rings, hard-edged sectors |
| Spec | Images 3 | Images 3 | Images 4 |
| Hard-stop trick gives you | Stripes | Rings | Wedges / pie slices |
| Gotcha | The angle convention | Size keywords change everything | Angles here are also clockwise from 12 o'clock |
Linear gradients โ and the angle convention
/* Default: top to bottom. No angle needed. */
background: linear-gradient(#4466EE, #E99537);
/* Keywords are the safe way to say it */
background: linear-gradient(to right, #4466EE, #E99537);
background: linear-gradient(to bottom right, #4466EE, #E99537);
/* Angles: 0deg = up, clockwise from there */
background: linear-gradient(90deg, #4466EE, #E99537); /* same as `to right` */
background: linear-gradient(180deg, #4466EE, #E99537); /* same as `to bottom` */
background: linear-gradient(135deg, #4466EE, #E99537); /* top-left โ bottom-right */
Commit this to memory and you'll never repeat my afternoon:
| Keyword | Angle |
|---|---|
to top |
0deg |
to right |
90deg |
to bottom |
180deg (the default) |
to left |
270deg |
There's one more subtlety with the corner keywords. to bottom right does not simply mean 135deg. The spec defines it as an angle chosen so that the gradient line is perpendicular to the other diagonal โ which means on a non-square box, to bottom right and 135deg produce visibly different results. On a square they're identical. That's the kind of thing you notice at 11pm when a card looks right in the design and wrong in a narrow column.
Radial gradients โ where the size keywords live
/* Simple */
background: radial-gradient(circle, #4466EE, #0F172A);
/* Positioned spotlight */
background: radial-gradient(circle at 30% 20%, #4466EE, transparent 60%);
/* Explicit size */
background: radial-gradient(circle closest-side at 60% 40%, #E99537, transparent);
The four size keywords decide where the final colour stop lands, measured from the centre:
closest-sideโ the nearest edge of the boxfarthest-sideโ the furthest edgeclosest-cornerโ the nearest cornerfarthest-cornerโ the furthest corner (this is the default)
Change the keyword and the gradient looks like a different gradient. This is the single most common reason a radial gradient "won't do what I want", and it is much faster to click through the four options in the Gradient Generator than to reason about it.
Conic gradients โ pies, wheels, and rings
Conic sweeps colour around the centre, starting at 12 o'clock and going clockwise (same convention as linear โ at least it's consistent).
/* Colour wheel */
background: conic-gradient(red, yellow, lime, aqua, blue, magenta, red);
/* Pie chart with hard stops โ 40% / 35% / 25% */
background: conic-gradient(
#4466EE 0deg 144deg,
#E99537 144deg 270deg,
#34D399 270deg 360deg
);
/* Progress ring: conic + a mask */
.ring {
background: conic-gradient(#4466EE 0turn 0.63turn, #E5E7EB 0.63turn 1turn);
mask: radial-gradient(circle, transparent 60%, #000 61%);
border-radius: 50%;
}
That mask line is the whole trick behind every donut progress indicator you've ever seen, and it's about six lines of CSS instead of an SVG library.
How Do I Control Where the Colours Land?
Colour stops
Give a stop a position and you control the transition. Give a stop two positions and you get a hard edge โ no interpolation at all.
/* Even (default) */
background: linear-gradient(to right, #4466EE, #E99537);
/* Explicit positions: solid blue to 30%, then blend */
background: linear-gradient(to right, #4466EE 30%, #E99537 70%);
/* Hard stops = stripes. The double-position syntax is Images 4. */
background: linear-gradient(
to right,
#4466EE 0 25%,
#E99537 25% 50%,
#34D399 50% 75%,
#0F172A 75% 100%
);
Interpolation hints (the midpoint you didn't know you could move)
A bare percentage between two colours moves the 50/50 midpoint. This is the most underused piece of gradient syntax I know:
/* Default: the halfway colour sits at 50% */
background: linear-gradient(to right, #4466EE, #E99537);
/* Halfway colour now sits at 25% โ blue gives way early */
background: linear-gradient(to right, #4466EE, 25%, #E99537);
Use it for image scrims. A to top, rgba(0,0,0,.8), transparent overlay usually eats too much of the photo; drop a 35% hint in and the darkness collapses toward the bottom where the caption is.
Repeating gradients for patterns
.hatched {
background: repeating-linear-gradient(
45deg,
#0F172A 0 10px,
#1E293B 10px 20px
);
}
One line, no image file, infinitely scalable. Just don't apply it to a full-page element and then wonder why scrolling stutters on a mid-range Android โ repainting a large repeating gradient during scroll is real work for the compositor.
Why Does My Gradient Look Muddy in the Middle?
Two separate causes, and people conflate them constantly.
Cause 1: sRGB interpolation
By default, browsers blend gradient stops in sRGB, channel by channel. For two colours that sit near each other on the wheel โ blue to purple, orange to red โ that's fine. For complementary colours it produces a dead grey band in the middle, because the mathematical midpoint of two opposed sRGB colours is roughly grey. Blue to yellow is the classic offender.
CSS Images Level 4 fixes this by letting you name the interpolation space:
/* Grey sludge in the middle */
background: linear-gradient(to right, blue, yellow);
/* Vivid all the way through */
background: linear-gradient(in oklch, to right, blue, yellow);
oklch keeps perceived lightness and chroma steady across the blend. Browser support for the in <colorspace> syntax is good in current Chrome, Safari, and Firefox, but if you need to support older browsers, the fallback is the old manual fix: add an intermediate stop and steer the blend yourself.
Cause 2: fading to transparent โ and my second mistake
transparent is defined as rgba(0, 0, 0, 0). Transparent black. If browsers interpolated naively, linear-gradient(#E99537, transparent) would slide through progressively darker, greyer orange on its way to nothing.
Modern browsers don't do that โ the spec requires interpolation in premultiplied alpha space, which sidesteps the problem entirely. But older WebKit did not, and I shipped a hero on a client site that looked clean on my machine and looked like a dirty smudge on their two-year-old iPad. The bulletproof version, which costs you nothing and still works everywhere:
/* Explicit: same colour, zero alpha */
background: linear-gradient(#E99537, rgba(233, 149, 55, 0));
I still write it that way out of scar tissue.
What Should I Watch Out for with Accessibility?
Contrast has to hold across the whole gradient
WCAG 2.1 SC 1.4.3 requires 4.5:1 for normal text and 3:1 for large text (18pt, or 14pt bold). A gradient means your background colour is different at every pixel, so the requirement has to be met against the worst-case point โ the lightest patch if your text is white, the darkest if it's black.
The reliable pattern is to stop trying to make the gradient itself readable, and layer a scrim on top:
.hero {
background:
linear-gradient(to bottom, rgba(15, 23, 42, 0.55), rgba(15, 23, 42, 0.8)),
linear-gradient(135deg, #4466EE, #E99537);
color: #fff;
}
Now the text sits on a known dark range regardless of what the gradient underneath is doing. Check the ends with the Color Picker before you ship โ the full contrast walkthrough, including the brand colour I shipped at 2.38:1, is in the Color Picker guide.
Gradient text is a trapdoor
This is the one that actually burned me on a production landing page:
.gradient-heading {
background: linear-gradient(135deg, #4466EE, #E99537);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
If background-clip: text doesn't apply for any reason โ an old browser, an extension blocking background images, a print stylesheet โ you're left with color: transparent and an invisible heading. The text is still in the DOM, so a screen reader reads it happily and you have no idea anything is wrong. Use @supports to gate it:
.gradient-heading { color: #0F172A; }
@supports (background-clip: text) or (-webkit-background-clip: text) {
.gradient-heading {
background: linear-gradient(135deg, #4466EE, #E99537);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
}
Solid colour by default, gradient as an enhancement. That's how it should have been written the first time.
Animated gradients need a motion escape hatch
You can't transition gradient colours โ the browser won't interpolate between two <image> values. What people animate instead is background-position across an oversized gradient:
.animated {
background: linear-gradient(270deg, #4466EE, #E99537, #34D399);
background-size: 600% 600%;
animation: shift 12s ease infinite;
}
@keyframes shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.animated { animation: none; }
}
That last block is not optional. Continuous background motion is a documented trigger for vestibular disorders, and honouring prefers-reduced-motion is three lines.
(The modern alternative is registering a custom property with @property so its type is known and therefore animatable, then using it inside the gradient. It's clean, it's supported in current browsers, and it's still worth having the reduced-motion guard.)
How Do I Build One with the Tool?
- Open the Gradient Generator.
- Pick a type โ linear, radial, or conic.
- Click a stop to change its colour; drag it to move its position. Click the bar to add a stop; drag it off to remove it.
- For linear, drag the angle. Watch the number: this is where the
0deg-is-up convention stops being abstract. - For radial, cycle the four size keywords and watch what changes. It's the fastest way to build intuition for them.
- Copy the CSS.
Two colours is usually the right answer. Every gradient I've regretted had five stops.
Gradients Worth Stealing
/* Toolz brand diagonal */
background: linear-gradient(135deg, #4466EE, #E99537);
/* Deep UI surface โ subtle enough that nobody calls it a gradient */
background: linear-gradient(to bottom, #1E293B, #0F172A);
/* Card lift: 4% of tonal difference, and it reads as depth */
background: linear-gradient(to bottom, #FFFFFF, #F8FAFC);
/* Mesh โ four positioned radials over a base colour */
background-color: #4466EE;
background-image:
radial-gradient(at 18% 24%, #E99537 0px, transparent 52%),
radial-gradient(at 79% 17%, #34D399 0px, transparent 48%),
radial-gradient(at 41% 83%, #8B5CF6 0px, transparent 55%);
/* Frosted glass */
background: linear-gradient(135deg, rgba(255,255,255,0.12), rgba(255,255,255,0.04));
backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.18);
Feed your own brand colours into the Color Picker first, take the HSL values, then vary lightness rather than hue โ that's how you get stops that blend cleanly instead of fighting.
Frequently Asked Questions
Why is my CSS gradient going the wrong direction?
Because CSS gradient angles start at the top and increase clockwise, unlike trigonometry, SVG, or canvas, which start at the right and go counter-clockwise. to right is 90deg, to bottom is 180deg, and a top-left to bottom-right diagonal is 135deg, not 45deg.
Do I still need -webkit- prefixes for CSS gradients?
Not for linear-gradient(), radial-gradient(), or conic-gradient() โ all three work unprefixed in every current browser. The one place the prefix still earns its keep is -webkit-background-clip: text for gradient text, where older Safari needs it. Ship both declarations there.
Can I animate a CSS gradient?
Not the gradient value itself โ a gradient is an <image>, and the browser won't interpolate between two images. The standard workaround is to make the gradient much larger than its box (background-size: 600% 600%) and animate background-position. The modern approach registers a custom property with @property so it has a known type and becomes animatable inside the gradient. Either way, guard it with prefers-reduced-motion.
Why does my gradient look grey or muddy in the middle?
Almost always sRGB interpolation between colours that sit opposite each other on the wheel โ blue to yellow is the classic. Their sRGB midpoint genuinely is close to grey. Fix it by naming a better interpolation space, linear-gradient(in oklch, to right, blue, yellow), or by adding an intermediate stop to steer the blend yourself.
Why does fading to transparent look dirty?
transparent is defined as rgba(0, 0, 0, 0) โ transparent black. Current browsers interpolate in premultiplied alpha space so this is no longer a problem, but older WebKit dragged the gradient through grey on its way out. If you support old devices, write the zero-alpha version of your own colour explicitly: rgba(233, 149, 55, 0) instead of transparent.
How do I make a gradient border?
Two options. border-image: linear-gradient(135deg, #4466EE, #E99537) 1; is one line but silently kills border-radius. If you need rounded corners, layer two backgrounds with different background-clip and background-origin values, or put the gradient on a pseudo-element behind the box.
Are CSS gradients bad for performance?
A static gradient is cheap โ cheaper than downloading an equivalent image, and it scales to any size for free. Costs show up when a large gradient has to be repainted repeatedly: a full-page repeating gradient during scroll, or several layered gradients under a backdrop-filter. If a page feels sticky on mobile, that's the first thing I'd check in DevTools' paint profiler.
How many colour stops should a gradient have?
Two, most of the time. Three when you want to steer the blend through a specific hue. Beyond that you're usually reaching for something a mesh gradient (several positioned radials) does better, and each extra stop is one more thing that can look wrong on someone else's display.
How do I make a gradient background in CSS?
Set the background (or background-image) property to a gradient function. background: linear-gradient(to right, #4466EE, #E99537); paints a left-to-right blend with no image file at all. radial-gradient() and conic-gradient() follow the same pattern for circular and swept blends. Because the result is generated rather than downloaded, it stays sharp at any size.
How do I create gradient text in CSS?
Put the gradient on background, then set background-clip: text (with the -webkit- prefix for Safari) and color: transparent, so the letters are filled by the gradient instead of a solid colour. Keep the real text in the markup rather than baking it into an image, so it stays selectable and readable by screen readers.
Wrapping Up
Gradients are one of the few CSS features that give you real visual richness for zero network cost โ no image request, no aspect-ratio problems, resolution-independent by construction. The syntax is small. The traps are specific and knowable: the angle convention, the radial size keywords, sRGB muddiness, and the invisible-heading trapdoor in gradient text.
Build them where you can see them. The Gradient Generator is free, runs entirely in your browser, and copies clean CSS. Pair it with the Color Picker for stops that hold their contrast, and the CSS Minifier when you're ready to inline it. The rest of the frontend shelf is in the Developer Productivity Tools guide.

