You know the moment. You're building a hero section, you drop in linear-gradient(to right, #667eea, #764ba2), and it looks... fine. Then you spend the next twenty minutes nudging hex values because the middle of the gradient has turned a weird muddy gray, and you're not totally sure why.
That muddy-middle problem, plus a handful of other things most gradient tutorials skip, is what this post is actually about. Linear and radial gradients are the 20% everyone learns. This covers the other 80% — repeating patterns, color-space interpolation, proper gradient animation, mesh gradients, and the small details that separate "it works" from "it looks intentional."
The three gradient types, at a glance
| Type | What it does | Best for |
|---|---|---|
linear-gradient() |
Transitions colors along a straight line | Hero backgrounds, buttons, overlays |
radial-gradient() |
Radiates colors from a center point | Spotlights, glows, vignettes |
conic-gradient() |
Sweeps colors around a center point | Pie charts, color wheels, loading spinners |
All three are treated as CSS images, which means anywhere background-image is valid, a gradient is valid too — including layered on top of real photos.
Linear gradients: the angle math nobody explains
.hero {
background: linear-gradient(to right, #ff6b6b, #4ecdc4);
}
You can direct a linear gradient with a keyword (to right, to bottom left) or a specific angle:
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
Here's the part that trips people up: 0deg points up, not right. Angles increase clockwise from there — 90deg is left-to-right, 180deg is top-to-bottom. If you've ever plugged in a "logical" angle and gotten the wrong direction, this is why.
Multiple color stops
.sunset {
background: linear-gradient(
to bottom,
#ff9a56 0%,
#ff6b6b 30%,
#c44569 60%,
#3d1e6d 100%
);
}
The percentages control exactly where each color hits full intensity — this is how you get more control than an even, evenly-spaced blend.
Hard stops (no blending)
Place two colors at the same percentage and you get a hard edge instead of a gradient — this is how diagonal stripe patterns are built without a single image file:
.stripes {
background: linear-gradient(
45deg,
#667eea 25%, #764ba2 25%,
#764ba2 50%, #667eea 50%,
#667eea 75%, #764ba2 75%
);
background-size: 40px 40px;
}
Radial gradients
.spotlight {
background: radial-gradient(circle, #ffeaa7, #2d3436);
}
You control shape (circle / ellipse), size, and position:
.glow {
background: radial-gradient(
circle at top left,
rgba(255, 255, 255, 0.3),
transparent 60%
);
}
at top left repositions the center — useful for faking a light source hitting a card from one corner.
Conic gradients
The newest of the three, and underused. Colors sweep around a center point like a pie chart:
.pie-chart {
background: conic-gradient(
#ff6b6b 0deg 90deg,
#4ecdc4 90deg 180deg,
#ffeaa7 180deg 270deg,
#a29bfe 270deg 360deg
);
border-radius: 50%;
}
That's a working pie chart with zero canvas or SVG. Most pure-CSS loading spinners and color-wheel pickers are conic gradients under the hood.
Repeating gradients (the part most tutorials skip)
All three gradient types have a repeating counterpart: repeating-linear-gradient(), repeating-radial-gradient(), and repeating-conic-gradient(). Instead of stretching a gradient once across an element, it tiles the pattern to fill the space.
/* Checkerboard-style stripes */
.repeating-stripes {
background: repeating-linear-gradient(
45deg,
#667eea 0px,
#667eea 10px,
#764ba2 10px,
#764ba2 20px
);
}
/* Concentric rings */
.target {
background: repeating-radial-gradient(
circle,
#ff6b6b 0px,
#ff6b6b 10px,
#ffffff 10px,
#ffffff 20px
);
}
/* Pie-slice loading spinner */
.spinner {
background: repeating-conic-gradient(
#667eea 0deg 10deg,
transparent 10deg 20deg
);
border-radius: 50%;
}
This is the technique behind most CSS-only progress rings, barcode-style patterns, and textured backgrounds — no repeated background image, no extra HTTP request.
Transparency: fading into nothing
Gradients don't need two solid endpoints — they can fade into full transparency, which is how readable text-over-image overlays get built:
.image-overlay {
background: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 0, 0, 0.8) 100%
);
}
Fixing the "muddy middle" problem: color-space interpolation
Remember the hero section from the intro? Here's what's actually happening. By default, browsers interpolate gradient colors in the sRGB color space, which produces desaturated, grayish midpoints — especially between colors that sit far apart on the color wheel, like blue and yellow.
Modern CSS lets you pick a different interpolation space:
/* Default sRGB — can look muddy in the middle */
.old-way {
background: linear-gradient(90deg, #3b82f6, #eab308);
}
/* OKLCH interpolation — smoother, more vivid transition */
.new-way {
background: linear-gradient(in oklch, 90deg, #3b82f6, #eab308);
}
oklch interpolates through a perceptually uniform color space, so the midpoint stays vivid instead of collapsing into gray. This single change is one of the most underrated gradient upgrades available right now — worth testing on any gradient that's looked slightly "off" without an obvious reason. It's well supported across current Chrome, Firefox, and Safari; the only thing to check is whether your project still needs to support noticeably older browser versions.
Gradient text
.gradient-text {
background: linear-gradient(90deg, #667eea, #764ba2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
background-clip: text uses the text glyphs as a mask, letting the gradient show through the letter shapes instead of filling as a solid color.
Gradient borders
Borders don't accept background directly, so this uses a layered-background workaround:
.gradient-border {
border: 4px solid transparent;
border-radius: 12px;
background:
linear-gradient(white, white) padding-box,
linear-gradient(45deg, #667eea, #764ba2) border-box;
}
One gradient fills the interior, the other fills just the border area — a genuinely elegant use of layered backgrounds.
Animating gradients, properly
The classic trick oversizes the gradient and animates its position:
.animated-gradient {
background: linear-gradient(270deg, #ff6b6b, #4ecdc4, #ffeaa7, #a29bfe);
background-size: 800% 800%;
animation: gradientShift 8s ease infinite;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
This works, but it's animating the position, not the gradient itself. Browsers can't natively interpolate between two arbitrary gradient definitions — which is why this workaround exists at all.
There's a more direct approach now: register the angle as a custom property with @property, which makes it genuinely animatable.
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.rotating-gradient {
background: linear-gradient(var(--angle), #ff6b6b, #4ecdc4);
animation: rotate 4s linear infinite;
}
@keyframes rotate {
to { --angle: 360deg; }
}
This actually rotates the gradient's angle frame by frame, rather than sliding a zoomed-in gradient around. @property is now well supported across current Chrome, Firefox, and Safari — if you're targeting older browser versions specifically, it's still worth a quick check, but for most projects today it's safe to ship as-is.
Mesh gradients
That soft, blurry, multi-color blob look you've been seeing on SaaS landing pages lately isn't a new CSS feature — it's just several radial gradients layered on top of each other:
.mesh-gradient {
background-color: #1a1a2e;
background-image:
radial-gradient(at 20% 20%, rgba(255, 107, 107, 0.6) 0px, transparent 50%),
radial-gradient(at 80% 0%, rgba(78, 205, 196, 0.5) 0px, transparent 50%),
radial-gradient(at 0% 100%, rgba(162, 155, 254, 0.5) 0px, transparent 50%),
radial-gradient(at 80% 100%, rgba(255, 234, 167, 0.4) 0px, transparent 50%);
}
Each radial gradient acts like a soft colored light source; stacking a few at different positions and opacities produces that trendy, organic blend. No image assets, no canvas — just layered backgrounds.
Practical checklist before you ship a gradient
- Contrast: if you're doing gradient text or a gradient button, check contrast at both ends of the gradient, not just the midpoint. A gradient readable at 20% but not at 80% will still fail an accessibility audit.
- Dark mode: a gradient tuned for light mode often looks muddy in dark mode. Define separate gradient variables per theme if your site supports both.
- Performance: gradients are cheap — no HTTP request, no image decode. If you're using a background image purely for a color wash, a gradient is almost always the better call.
-
Browser support: linear, radial, and conic gradients,
in oklchinterpolation, and@property-based animation are all well supported in current Chrome, Firefox, and Safari. If your project needs to support noticeably older browser versions, caniuse.com is the fastest way to double-check a specific feature.
Wrapping up
Gradients reward going one level past the basics. Understanding angles properly, knowing when interpolation is making your colors muddy, and having repeating and conic gradients in your toolkit means you can replace a surprising number of image assets with a few lines of CSS — lighter pages, and effects that are trivial to tweak later without touching a design tool.
If you'd rather compose a gradient visually before writing the CSS by hand — dragging stops, adjusting the angle, previewing oklch vs. sRGB — that's a workflow a visual gradient builder handles well; this one is one option if you want to skip the manual math.
For the full technical reference on interpolation and gradient syntax, MDN's gradient documentation is the best place to go deeper.
What's the trickiest gradient effect you've had to build? Curious what's tripped other people up.
Top comments (0)