Skip to content
DevDepth

In-depth React internals and CSS layout guides for frontend engineers.

← Back to all articles

In-Depth Article

How CSS Gradients Really Work: `linear-gradient()`, `radial-gradient()`, `conic-gradient()`, and Repeating Patterns

Learn how CSS gradients actually resolve color stops, when to use linear, radial, conic, or repeating gradients, and how to build gradient text, borders, masks, and animations without common rendering traps.

Published: Updated: 9 min readlayout-strategy
css-gradientslinear-gradientconic-gradientbackground-imagelayout-strategy

Gradients show up everywhere in modern UI, but they are easy to underestimate.

At first glance, a gradient looks like "just a color effect". In production, it behaves more like a generated image with its own geometry, sizing rules, interpolation quirks, and rendering edge cases.

That is why gradients can feel surprisingly slippery:

  • a stop lands in the wrong place
  • a stripe pattern breaks at a new angle
  • a fade to transparent looks muddier than expected
  • a beautiful gradient text demo falls apart across multiple lines
  • an animated gradient works only after you stop animating the gradient directly

This guide focuses on the parts that actually matter when you build with gradients in real UI:

  • when to use linear, radial, conic, or repeating gradients
  • how color stops resolve, even when you do not set them explicitly
  • why gradients are images, not colors
  • how to build gradient text, borders, textures, and masks
  • how to avoid the rendering traps that make gradients harder than they should be

If your gradient is mainly there to protect text over photography, pair this with How to Make Text Over Images Readable in CSS. If the design also depends on rounded geometry, How border-radius Really Works: Percentages, Nested Corners, Overlap, and Transform is the best companion page.

1. Gradients are images, not color values

This is the first idea that makes the rest of CSS gradients easier to reason about.

Gradient functions return an <image> value. That means they do not belong on properties that expect a plain <color>, but they do work anywhere CSS accepts an image-like input.

That includes places such as:

  • background-image
  • border-image
  • mask-image
  • list-style-image

So this works:

.card {
  background-image: linear-gradient(135deg, #ff8a00, #e52e71);
}

And this does not:

.card {
  color: linear-gradient(135deg, #ff8a00, #e52e71);
}

That also explains why stacked gradients behave like stacked background images:

.surface {
  background-image:
    radial-gradient(circle at top right, rgb(255 255 255 / 0.25), transparent 40%),
    linear-gradient(180deg, #1f2430, #10141d);
}

Once you treat gradients as generated images, not magical color shortcuts, a lot of CSS behavior starts making sense.

2. Pick the gradient type by the shape of the problem

CSS gives you three core gradient families, plus a repeating version of each.

Use linear-gradient() for direction

Use a linear gradient when the effect should move along a line:

  • hero backgrounds
  • button fills
  • edge fades
  • scrims under text
  • directional lighting
.hero {
  background-image: linear-gradient(to right, #0f172a, #1d4ed8);
}

Use radial-gradient() for focus

Use a radial gradient when the effect should radiate from a point:

  • glows
  • spot highlights
  • soft vignettes
  • faux lighting
.spotlight {
  background-image: radial-gradient(circle at center, rgb(255 255 255 / 0.5), transparent 60%);
}

Use conic-gradient() for rotation

Use a conic gradient when the effect is naturally angular:

  • color wheels
  • progress rings
  • pie-style visuals
  • angular decorative backgrounds
.wheel {
  background-image: conic-gradient(from 180deg, #f59e0b, #ec4899, #3b82f6, #f59e0b);
}

Use repeating gradients for patterns

Use repeating-linear-gradient(), repeating-radial-gradient(), or repeating-conic-gradient() when the result is meant to tile by definition:

  • stripes
  • grids
  • checkerboards
  • texture patterns
  • angular repeats
.stripes {
  background-image: repeating-linear-gradient(
    90deg,
    #111827 0 12px,
    #1d4ed8 12px 24px
  );
}

The question is not "which one looks nicest in the demo?" It is "what geometry is this effect actually following?"

3. Color stops always resolve to real positions

Every gradient stop has a color and a position, even when you omit the position in your code.

That makes this:

.banner {
  background-image: linear-gradient(to right, #f5bb43, #c668ec, #e03cb0, #9e6c6a, #fce44d);
}

behave as if the colors were distributed evenly from the start of the gradient line to the end.

In practice:

  • the first stop defaults to 0%
  • the last stop defaults to 100%
  • missing stops in between are distributed across the remaining space

That means five colors create four intervals, so the middle stops land at evenly spaced points along the gradient line.

This matters because a gradient is not just "mix these colors somehow". It is "mix these colors along a measurable line or arc".

4. Mixed units and out-of-order stops are where gradients get weird

Two common gradient bugs come from the same root cause: stop positions are real positions, not hints.

Mixed units can shift unexpectedly

This looks innocent:

.panel {
  background-image: linear-gradient(to right, #2563eb 100px, #ec4899 50%);
}

But 100px and 50% only line up the way you expect at certain element sizes. On a narrower element, the percentage stop can end up earlier than the fixed-length stop.

That can create hard transitions or a stop correction you did not intend.

Stops do not move backward

If a later stop is placed before an earlier one, the later stop gets clamped forward rather than pulling the earlier stop backward.

That is why this creates a hard edge:

.panel {
  background-image: linear-gradient(
    to right,
    #2563eb 20%,
    #ec4899 0%,
    #f59e0b 40%
  );
}

The second stop cannot stay at 0% once the first stop is already at 20%, so it resolves at the same position as the previous stop.

That is useful when you want a crisp band. It is confusing when you thought you were defining a soft blend.

5. Angles change the gradient line, not just the direction label

With linear gradients, the angle affects the actual length and travel of the gradient line through the box.

That is why a 45deg gradient on a square and a 45deg gradient on a wide rectangle do not feel identical. The color transition still follows the same angle, but the path through the element is different.

This is also why directional keywords such as to top right are often more intuitive than memorizing raw angles. They describe the visual intent directly.

Use explicit angles when:

  • the design tool gave you one
  • the effect is rotation-driven
  • you are matching a pattern or repeated motif

Use directional keywords when:

  • you care more about the visual destination than the raw math
  • the component should stay easy to read later

6. Repeating gradients are not just shorthand for background-size

You can fake some repeating effects with a normal gradient plus background-size and background-repeat, but that is not the same thing as a repeating gradient.

For example, stripes:

.fake-stripes {
  background-image: linear-gradient(
    90deg,
    #111827 0 50%,
    #1d4ed8 50% 100%
  );
  background-size: 24px 100%;
  background-repeat: repeat-x;
}

That can work for simple cases, but the moment you change angles, element ratios, or want more complex repeat behavior, the mental model gets awkward fast.

A repeating gradient is cleaner because the repeat length is built into the stop list itself:

.real-stripes {
  background-image: repeating-linear-gradient(
    90deg,
    #111827 0 12px,
    #1d4ed8 12px 24px
  );
}

The key rule is:

a repeating gradient repeats the distance between the first stop and the last stop.

That becomes the basic unit that tiles forever.

This is especially useful for:

  • stripes at arbitrary angles
  • checkerboard-like textures
  • repeating radial ripples
  • repeating conic wedges

7. Gradients are much more useful than "background flair"

Developers often reach for gradients only when a page needs a glossy background. They are much more versatile than that.

Gradient text

You cannot assign a gradient directly to color, but you can fill text with a gradient by painting the background through the glyphs:

.gradient-text {
  background-image: linear-gradient(90deg, #09f1b8, #00a2ff, #ff00d2, #fed90f);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

For single-line headlines, that is usually enough.

For multi-line inline text where each line should get its own consistent gradient fill, see section 11.

Gradient borders

You can create gradient borders with border-image:

.ring {
  border: 4px solid transparent;
  border-image: linear-gradient(90deg, #ec4899, #8b5cf6) 1;
}

In production UI, many teams prefer a layered-background technique because it is easier to reason about on rounded components:

.ring {
  border: 4px solid transparent;
  border-radius: 1rem;
  background:
    linear-gradient(#0f172a, #0f172a) padding-box,
    linear-gradient(90deg, #ec4899, #8b5cf6) border-box;
}

If the component depends on very precise inside or outside stroke behavior, How to Create Inside, Outside, and Center Borders in CSS is the right follow-up.

Texture and pattern backgrounds

Gradients are great for generated texture because they scale with the element instead of shipping an extra asset:

.checker {
  background:
    repeating-conic-gradient(#000 0% 25%, #eee 0% 50%) 50% / 40px 40px;
}

This is where repeating gradients really shine. They let you build stripes, dots, wedges, and grid-like textures with less brittle math than image slicing.

Masks and cutout effects

Because gradients are images, they can drive masks too:

.cutout {
  mask-image: linear-gradient(
    to right,
    #000 0 50%,
    transparent 50% 100%
  );
  mask-size: 12vw 100%;
  mask-repeat: repeat;
}

That makes gradients useful for:

  • reveal effects
  • cutout textures
  • directional fades
  • hover masks

8. The most common gradient pitfall is an ugly fade to transparent

This looks like the obvious way to fade red out:

.fade {
  background-image: linear-gradient(to bottom, red, transparent);
}

But transparent is technically rgb(0 0 0 / 0%), which can produce a muddier transition than you expected, especially when the start color is vivid.

MDN documents transparent as a shortcut for fully transparent black, and notes that older browsers may treat it literally as black with zero alpha rather than interpolating the way you hoped.

The safer pattern is to fade to the transparent version of the same hue:

.fade {
  background-image: linear-gradient(
    to bottom,
    rgb(255 0 0 / 1),
    rgb(255 0 0 / 0)
  );
}

Or with hex:

.fade {
  background-image: linear-gradient(to bottom, #ff0000, #ff000000);
}

This is one of the simplest ways to make a gradient look more intentional immediately.

Source: MDN named color reference, which notes that transparent is a shortcut for rgb(0 0 0 / 0%).

9. Muddy midpoints and banding need different fixes

Not every bad gradient is the same bad gradient.

Muddy midpoint

Sometimes two colors blend through a midpoint that feels dull or grayish. The fix is usually to guide the interpolation instead of letting the browser travel straight from color A to color B with no help.

Practical fixes:

  • add one or more intermediate stops
  • choose an intentionally warmer or cooler midpoint
  • use a generated easing-style gradient when you need a more art-directed transition
.sunset {
  background-image: linear-gradient(
    90deg,
    #1f005c,
    #5b0060,
    #870160,
    #ac255e,
    #ca485c,
    #e16b5c,
    #f39060,
    #ffb56b
  );
}

Banding

Banding is different. That is when you can see visible step lines in what should feel like a smooth blend.

Practical fixes:

  • add more intermediate stops
  • use easing-style generated stop lists
  • add subtle noise or texture above the gradient
  • avoid giant smooth gradients with only two far-apart stops when visual quality matters

You are not fixing the same problem twice. Muddy color travel and visible banding are related, but they are not identical.

10. Raw gradient animation is limited, but registered custom properties help

A lot of developers eventually discover that animating from one full gradient string to another is inconsistent or disappointing.

The more reliable pattern is to animate the parts that drive the gradient:

  • a color
  • a stop position
  • an angle

That is where @property becomes genuinely useful.

MDN documents @property as a way to register a custom property with a known syntax, which lets the browser interpolate typed values such as percentages, lengths, colors, and angles.

For example, animate a stop position:

@property --progress {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 25%;
}

.bar {
  --progress: 25%;
  background-image: linear-gradient(
    to right,
    #00d230 var(--progress),
    #111827 var(--progress)
  );
  animation: fill 2.5s ease infinite alternate;
}

@keyframes fill {
  to {
    --progress: 100%;
  }
}

You can do the same for an angle:

@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 90deg;
}

.surface {
  --angle: 90deg;
  background-image: linear-gradient(var(--angle), #ec4899, #3b82f6);
  animation: spin-gradient 3s linear infinite alternate;
}

@keyframes spin-gradient {
  to {
    --angle: 180deg;
  }
}

That approach is much more predictable than trying to tween the whole gradient declaration as a single opaque string.

Source: MDN @property, including its example of animating a gradient stop with a registered custom property.

11. Multi-line gradient text needs box-decoration-break: clone

Single-line gradient text is easy enough. Multi-line gradient text is where developers start noticing that the background belongs to the whole element, not each line fragment.

Without extra help, this:

.copy {
  background-image: linear-gradient(135deg, deeppink, dodgerblue);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

usually creates one continuous gradient across the entire text box. That means each wrapped line can land on a different part of the same gradient.

If you want each line fragment to paint its own copy of the background, use:

.copy {
  background-image: linear-gradient(135deg, deeppink, dodgerblue);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

MDN notes that box-decoration-break: clone draws the background independently for each fragment, which is exactly why it helps here.

Source: MDN box-decoration-break.

12. A practical checklist before you ship a gradient-heavy component

Before publishing a gradient-heavy UI, check:

  1. Is this really a gradient problem, or would a flat color be clearer and cheaper?
  2. Did you choose the right gradient family for the geometry of the effect?
  3. Are your stop units consistent enough to stay stable across different component sizes?
  4. If the fade ends at transparency, are you using a transparent version of the same hue instead of generic transparent?
  5. If the result looks muddy, would an intermediate stop fix it better than changing the whole palette?
  6. If the result looks banded, do you need more stops or a touch of texture?
  7. If the gradient animates, are you animating typed custom properties instead of the raw gradient string?
  8. If the gradient is applied to text, did you test wrapped lines, not just the single-line demo?

That checklist catches most of the mistakes that make gradients feel mysterious later.

13. The takeaway

CSS gradients are easy to start and surprisingly deep to finish well.

They are not just decorative color washes. They are generated images with geometry, stop positions, repetition rules, and interpolation behavior.

If you keep three ideas from this page, make them these:

  • gradients are images, not colors
  • stop positions are real positions, not vague hints
  • repeating gradients exist because they solve a real geometry problem, not just a syntax problem

Once those pieces click, gradients stop feeling like trial and error and start feeling like a dependable layout and visual tool.

Publisher and editor

DevDepth Publisher

Independent publisher and frontend engineering writer

DevDepth is maintained as an independent frontend engineering publication focused on React internals, CSS layout systems, and production debugging guides.

React internalsCSS layout systemsfrontend debuggingrendering and performance reasoning

Each article is reviewed before publication for technical accuracy, explanation quality, metadata clarity, and internal-link fit within the current archive.

Last editorial review: Mar 20, 2026