Six production-ready CSS gradient patterns with copy-paste code: OKLCH interpolation, off-screen glows, hard stops, layered blends, text, and borders.

The same boring tutorial has been ranking for “css gradient examples” for a decade: two colors, one angle, done. That’s not how modern UI gradients are built anymore. Here are six patterns developers are actually shipping right now, each with complete, copy-paste CSS—and if you want to sketch the first one visually, you can build the same gradient interactively.
If your gradients still come from a mental template like “pick two colors and point them left to right,” you’ve probably seen the same failures over and over:
A typical scenario is a dark hero section where you want the polished look of Stripe-, Linear-, or Vercel-style backgrounds. You try a basic linear gradient, then stack random color stops until it sort of works. The result usually feels flat because the effect isn’t just “a gradient”—it’s a specific pattern with a specific trick behind it.
The fix is to stop treating gradients as one feature and start treating them as a toolbox. Modern production gradients usually fall into a handful of repeatable categories: better interpolation, off-canvas radial positioning, hard-stop segmentation, layered blends, clipped text, and border techniques that fit the shape you actually need. Once you know which pattern solves which visual problem, the CSS becomes much more predictable.
The fastest way to make a gradient look dated is to blend vivid colors through the default interpolation path and accept the muddy midpoint. Complementary or near-complementary colors are especially bad at this.
Use these two declarations side by side:
.gradient-srgb {
background: linear-gradient(to right, oklch(0.55 0.22 264), oklch(0.65 0.24 29));
}
.gradient-oklch {
background: linear-gradient(in oklch to right, oklch(0.55 0.22 264), oklch(0.65 0.24 29));
}Here’s a runnable demo showing both blocks:
<div class="demo">
<div class="swatch gradient-srgb">Default interpolation</div>
<div class="swatch gradient-oklch">OKLCH interpolation</div>
</div>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: oklch(0.16 0.03 260);
font-family: Inter, system-ui, sans-serif;
color: white;
}
.demo {
width: min(900px, 92vw);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}
.swatch {
min-height: 180px;
border-radius: 20px;
display: grid;
place-items: end start;
padding: 20px;
font-weight: 600;
box-shadow: 0 20px 60px oklch(0.05 0.01 260 / 0.45);
}
.gradient-srgb {
background: linear-gradient(to right, oklch(0.55 0.22 264), oklch(0.65 0.24 29));
}
.gradient-oklch {
background: linear-gradient(in oklch to right, oklch(0.55 0.22 264), oklch(0.65 0.24 29));
}
</style>The difference is simple: in oklch changes the route the browser takes between the two endpoints. Instead of falling through a dull midpoint, the blend stays more vivid across the full ramp.
For older-browser fallback, manually place a midpoint stop that preserves the energy you want:
.gradient-fallback {
background: linear-gradient(
to right,
oklch(0.55 0.22 264) 0%,
oklch(0.68 0.24 330) 50%,
oklch(0.65 0.24 29) 100%
);
}That’s not mathematically identical to OKLCH interpolation, but it avoids the “why did this turn brown in the middle?” problem. If you want to tune the stop positions visually instead of nudging percentages in DevTools, build the same gradient interactively.
When you center a radial gradient inside the element, you usually get a visible hotspot. That’s fine for a spotlight. It’s wrong for ambient UI glow.
The production trick is to move the center outside the element so only the feathered edge shows up:
.hero {
background:
radial-gradient(circle at 120% -20%, oklch(0.65 0.2 280 / 0.5), transparent 55%),
radial-gradient(circle at -20% 120%, oklch(0.65 0.22 190 / 0.4), transparent 55%),
#0f172a;
}Here’s a complete example:
<section class="hero">
<div class="content">
<p class="eyebrow">Ship ambient light, not a bullseye</p>
<h1>Dark hero backgrounds need the glow source off-screen.</h1>
<p>
Position the radial center beyond the element boundary so the visible area
only catches the soft fade-in edge.
</p>
</div>
</section>
<style>
body {
margin: 0;
font-family: Inter, system-ui, sans-serif;
background: #020617;
color: white;
}
.hero {
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px;
background:
radial-gradient(circle at 120% -20%, oklch(0.65 0.2 280 / 0.5), transparent 55%),
radial-gradient(circle at -20% 120%, oklch(0.65 0.22 190 / 0.4), transparent 55%),
#0f172a;
}
.content {
max-width: 720px;
}
.eyebrow {
color: oklch(0.8 0.08 200);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.8rem;
}
h1 {
font-size: clamp(2.5rem, 6vw, 5rem);
line-height: 1;
margin: 0.2em 0;
}
p {
color: oklch(0.86 0.02 255);
font-size: 1.1rem;
}
</style>The key point is not the color choice. It’s the coordinates: 120% -20% and -20% 120%. Those push the centers off-canvas so the effect reads as environmental light, not a decorative circle. Use this for hero sections, dark cards, floating panels, and subtle button halos.
Soft gradients get most of the attention, but hard stops are where gradients start replacing assets.
First pattern: a stripe texture using repeating-conic-gradient().
.stripes {
background: repeating-conic-gradient(
#1e293b 0deg 45deg,
#334155 45deg 90deg
);
}To keep the post aligned with modern color usage, here’s the same pattern in OKLCH values and with sizing so it becomes a usable texture:
<div class="panel stripes"></div>
<style>
.panel {
width: 280px;
height: 280px;
border-radius: 20px;
background:
repeating-conic-gradient(
oklch(0.28 0.03 255) 0deg 45deg,
oklch(0.36 0.03 255) 45deg 90deg
);
background-size: 24px 24px;
}
</style>The crisp boundary comes from using the same position for the end of one segment and the start of the next. No blur, no fade, no pseudo-elements.
Second pattern: a donut progress ring with conic-gradient() plus a radial mask.
.donut {
--progress: 65%;
background: conic-gradient(
oklch(0.65 0.2 160) 0% var(--progress),
#1e293b var(--progress) 100%
);
border-radius: 50%;
-webkit-mask: radial-gradient(circle, transparent 55%, black 56%);
mask: radial-gradient(circle, transparent 55%, black 56%);
}Complete runnable version:
<div class="donut-wrap">
<div class="donut"></div>
</div>
<style>
.donut-wrap {
display: grid;
place-items: center;
width: 220px;
height: 220px;
background: oklch(0.18 0.03 255);
border-radius: 24px;
}
.donut {
--progress: 65%;
width: 140px;
aspect-ratio: 1;
background: conic-gradient(
oklch(0.65 0.2 160) 0% var(--progress),
#1e293b var(--progress) 100%
);
border-radius: 50%;
-webkit-mask: radial-gradient(circle, transparent 55%, black 56%);
mask: radial-gradient(circle, transparent 55%, black 56%);
}
</style>This is useful when you need a small stats chip or dashboard ring and don’t want to bring in SVG just to paint one arc.
A lot of “premium” gradient surfaces are really three or four gradients stacked together and blended. One layer gives direction, another gives a soft hotspot, another provides a tonal base.
Use this exact stack:
.mesh-card {
background:
linear-gradient(135deg, oklch(0.65 0.2 280 / 0.6), transparent 50%),
radial-gradient(circle at 80% 20%, oklch(0.7 0.18 190 / 0.5), transparent 50%),
conic-gradient(from 180deg in oklch, oklch(0.15 0.02 264), oklch(0.2 0.03 280));
background-blend-mode: overlay, color-dodge, normal;
}Full card example:
<article class="mesh-card">
<span class="badge">Mesh background</span>
<h2>Three gradients, one background stack.</h2>
<p>Use blend modes to create depth without image assets or canvas rendering.</p>
</article>
<style>
.mesh-card {
width: min(560px, 92vw);
padding: 32px;
border-radius: 24px;
color: white;
background:
linear-gradient(135deg, oklch(0.65 0.2 280 / 0.6), transparent 50%),
radial-gradient(circle at 80% 20%, oklch(0.7 0.18 190 / 0.5), transparent 50%),
conic-gradient(from 180deg in oklch, oklch(0.15 0.02 264), oklch(0.2 0.03 280));
background-blend-mode: overlay, color-dodge, normal;
box-shadow: 0 20px 80px oklch(0.05 0.01 260 / 0.45);
}
.badge {
display: inline-block;
margin-bottom: 16px;
padding: 6px 10px;
border-radius: 999px;
background: oklch(0.98 0 0 / 0.08);
font-size: 0.85rem;
}
.mesh-card h2 {
margin: 0 0 12px;
font-size: 2rem;
}
.mesh-card p {
margin: 0;
color: oklch(0.9 0.02 255);
}
</style>The practical rule: start with a dark conic or linear base, add one directional accent, then add one radial light source. If the result looks noisy, reduce alpha before changing the blend mode.
Gradient text is still one of the easiest ways to make a heading feel intentional instead of default. The breakage point is also still the same: Safari needs the prefixed clip property.
Use the complete snippet, not the almost-complete one:
.gradient-heading {
background: linear-gradient(in oklch to right, oklch(0.65 0.22 280), oklch(0.7 0.2 160));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}Runnable example:
<h1 class="gradient-heading">Ship brighter headings without image assets.</h1>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: oklch(0.16 0.03 260);
font-family: Inter, system-ui, sans-serif;
}
.gradient-heading {
margin: 0;
font-size: clamp(3rem, 8vw, 6rem);
line-height: 0.95;
background: linear-gradient(in oklch to right, oklch(0.65 0.22 280), oklch(0.7 0.2 160));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
</style>Two production notes matter here. First, don’t set a separate text color later in the cascade or you’ll kill the effect. Second, this works best on large text where the gradient has room to travel; on small labels it usually looks accidental.
For square corners or strict boxes, the modern native move is border-image. It’s clean and short:
.gradient-border {
border: 2px solid transparent;
border-image: linear-gradient(in oklch to right, oklch(0.65 0.22 280), oklch(0.7 0.2 160)) 1;
}Use it like this:
<div class="gradient-border">Fast native border for square corners</div>
<style>
.gradient-border {
display: inline-block;
padding: 16px 20px;
background: oklch(0.18 0.03 255);
color: white;
border: 2px solid transparent;
border-image: linear-gradient(in oklch to right, oklch(0.65 0.22 280), oklch(0.7 0.2 160)) 1;
}
</style>The limitation is real: border-image does not support border-radius the way you want. For rounded cards, buttons, or pills, switch to a pseudo-element:
.gradient-border-rounded {
position: relative;
border-radius: 12px;
background: #0f172a;
}
.gradient-border-rounded::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 14px;
background: linear-gradient(in oklch to right, oklch(0.65 0.22 280), oklch(0.7 0.2 160));
z-index: -1;
}Complete version:
<div class="gradient-border-rounded">Rounded gradient border that actually behaves</div>
<style>
.gradient-border-rounded {
position: relative;
display: inline-block;
padding: 16px 20px;
border-radius: 12px;
background: #0f172a;
color: white;
isolation: isolate;
}
.gradient-border-rounded::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 14px;
background: linear-gradient(in oklch to right, oklch(0.65 0.22 280), oklch(0.7 0.2 160));
z-index: -1;
}
</style>If you need simple and square, use border-image. If you need rounded corners, layered backgrounds, or more control over hover states, use the pseudo-element.
These patterns are production-ready, but each one has a sharp edge.
OKLCH interpolation is the best default when you want vivid blends, but if your design system still defines everything in legacy hex tokens, you may need a small token translation step before gradients feel consistent. The manual midpoint fallback works, but it becomes design work, not just syntax.
Off-screen radial glows are excellent for heroes and dark surfaces, but they can get muddy fast if you stack too many large translucent layers. Two is usually enough. More than that, and the page starts looking foggy instead of luminous.
Hard-stop conic patterns are crisp and cheap, but they can alias at small sizes or feel too mechanical if the segment size is too regular. For stripes, vary scale and opacity before adding more colors. For donut rings, remember that masks and inner cutouts can expose background assumptions if the component gets dropped onto a different surface.
Layered blend-mode backgrounds are powerful, but they’re also the easiest way to create accidental visual noise. The usual mistake is maxing out chroma and opacity on every layer. Treat one layer as the star and the others as support. If the card competes with its own content, the background is overbuilt.
Gradient text remains a display treatment, not a body-text pattern. It reduces flexibility for selection states, forced-colors modes, and some edge-case rendering paths. Use it on headings, not paragraphs. And keep the -webkit-background-clip: text line every time, because that’s still the production footgun.
Gradient borders are the most conditional pattern of the six. border-image is elegant but limited; pseudo-elements are flexible but add positioning complexity and require care with stacking contexts. If the component already has shadows, overlays, or transforms, check that the pseudo-element still sits where you expect.
The old tutorial pattern was “pick two colors and rotate the angle until it feels less bad.” These six patterns are the version that actually ships: vivid interpolation, off-canvas glow placement, hard-stop segmentation, layered blend stacks, clipped text, and borders that match the component shape. If you want the fast path, the Gradient Generator handles the math—choose the gradient type, pick OKLCH interpolation, adjust stops, and copy the output straight into your project.