You paste in three lines from a Stack Overflow answer, refresh the page, and your heading shows up as a solid block of color. Or worse, a colored rectangle where the letters used to be. No error, no warning in the console, nothing in the network tab. The CSS is "valid." It just doesn't do what it looks like it should.
This failure mode shows up constantly, and it isn't a quirk of your stylesheet. It's a side effect of how gradient text actually works under the hood: there is no gradient-text property in CSS, and there never was. The whole effect is held together by a property that was invented for something else entirely, in 2008, by one browser, for one platform. Everything that goes wrong with gradient text traces back to that origin.
The property was never meant to clip text
background-clip exists to answer a narrow question: where should an element's background stop? By default a background paints out to the border edge. Set background-clip: padding-box and it stops at the padding edge instead; content-box clips it to the content area. These three values map cleanly onto the box model and have been part of CSS Backgrounds and Borders Level 3 since the spec reached Candidate Recommendation in September 2014.
There is a fourth value, text, and it does something categorically different. Instead of clipping the background to a box edge, it clips the background to the *shape of the rendered glyphs*. The text becomes a stencil. Whatever you painted as the background, now only shows through the cut-out letterforms.
That value did not come from the CSS Working Group. Apple shipped it in WebKit in 2008 and it landed in Safari 3.2 the same year, as -webkit-background-clip: text. It was a vendor extension, useful enough that it spread, and the working group only later went back to standardize the behavior other browsers had already copied. The standard followed the implementation, not the other way around, which is why the property feels slightly off-pattern compared to the rest of the box model. It is a typographic effect wearing a layout property's clothes.
Three declarations, and why missing one fails silently
To get gradient letters you need three things to be true at once, and each one is doing a job most people don't think about:
h1 {
background: linear-gradient(90deg, #ff2d6f, #7c5cff);
background-clip: text;
-webkit-text-fill-color: transparent;
}The first line paints the gradient. At this point the gradient is sitting behind the text, filling the whole element box. The letters are painted on top of it in their normal color, so you see solid-colored text on a gradient rectangle.
The second line clips that gradient to the glyph shapes. Now the gradient only exists inside the letterforms. But the letters are *still* painted in their normal text color, on top of the clipped gradient, so they completely hide it. You see solid text and no gradient at all.
The third line is the one everybody forgets. -webkit-text-fill-color: transparent knocks out the actual ink of the glyphs so the clipped gradient underneath can show through. Without it, you have done all the work and rendered an invisible gradient behind opaque letters.
The reason any one of these failing produces a *plausible-looking* result rather than an error is that each declaration is independently valid CSS. Drop the third line and you get solid text. Drop the second and you get a gradient box. Drop the first and you get transparent, invisible text. The browser has no concept that these three lines are meant to compose into a single effect, so it can't tell you the composition is incomplete. It just renders whatever subset you gave it.
That last failure, transparent invisible text, is the one that bites in production. If you ship -webkit-text-fill-color: transparent without a working clipped gradient behind it (say the gradient failed to load, or a later rule overrode background), the text vanishes entirely. A heading that should read "Pricing" becomes empty space, and screen readers still announce it while sighted users see nothing.
color versus -webkit-text-fill-color
Here is a trap that survives even when the three core lines are present. You'd assume color controls the visible color of text, and normally it does. But -webkit-text-fill-color is more specific than color for the actual glyph fill. When both are set, the fill color wins.
This matters for fallbacks. A common instinct is to set a color as a safety net so that if the gradient breaks, the text shows in a readable solid color. That instinct is correct, but the order and the property matter:
| Declaration | What it controls | Wins when both set |
|---|---|---|
color: #333 | Text color, and the inherited fallback | No |
-webkit-text-fill-color: transparent | The actual painted glyph fill | Yes |
If you set color: #333 as a fallback *and* -webkit-text-fill-color: transparent, browsers that understand the fill property will still paint transparent text. Your fallback never fires in the browser you were worried about. The correct pattern is to let color carry the readable fallback and only apply -webkit-text-fill-color: transparent inside an @supports block, or to scope the transparent fill behind a feature query so non-supporting engines keep the solid color.
The prefix history is still in your CSS today
You will notice every working snippet still carries -webkit-text-fill-color with the prefix, and many still write -webkit-background-clip: text alongside the unprefixed version. That isn't superstition. It's the long tail of a property that lived behind a vendor prefix for fifteen years.
The rough timeline:
| Year | Event |
|---|---|
| 2008 | Apple ships -webkit-background-clip: text in Safari 3.2 |
| 2010 | Safari unprefixes background-clip (the box values) in Safari 5 |
| 2016 | Firefox 49 supports background-clip: text |
| Dec 2023 | Chrome and Edge 120 finally drop the -webkit- requirement for the text value |
| May 2024 | Samsung Internet 24 unprefixes |
For most of that span, Chromium honored only -webkit-background-clip: text. Code written before December 2023, which is most of the code on the web, uses the prefix because it had to. Chrome 119 and earlier, Safari 15.4 and earlier, and Samsung Internet 23 and earlier all still need it. So the defensive pattern is to write both the prefixed and unprefixed background-clip, and to always write -webkit-text-fill-color (which never got an unprefixed standard equivalent that's widely shipped). The result is a snippet that looks redundant but isn't.
Why gradients go muddy through the middle
Once the clipping works, the next surprise is aesthetic rather than structural. You pick a vivid pink and a vivid teal, and the gradient between them passes through a dull, washed-out grey-brown in the middle. The endpoints look great; the transition looks like it died.
This is interpolation, and the color space you interpolate in decides the path the gradient takes. The default for plain hex and rgb() gradients is sRGB, and a straight line through sRGB between two saturated colors dips toward the desaturated center of the color volume. Pink to teal genuinely routes through a muddy zone because the shortest numeric path between those two RGB triples happens to pass near grey.
OKLCH, standardized in CSS Color Module Level 4, is built around perceived lightness and hue instead of raw signal values. Interpolating in OKLCH keeps lightness even and walks the hue around the wheel, so the midpoint of a pink-to-teal gradient stays luminous instead of collapsing. The difference is most visible exactly where it hurts most on text: the thin strokes of a glyph at the gradient's center.
The catch is browser support. OKLCH gradient syntax has full support only in Chrome 111 and up, Firefox 113 and up, and Safari 15.4 and up. Below that, an oklch() color in a gradient is ignored. So gradient text using OKLCH needs a hex-based fallback declaration commented or feature-queried in, the same defensive instinct as the prefix story, applied one layer up.
The box-versus-glyph mismatch
A final, less-discussed edge case: the gradient is sized to the element's box, not to the text. background-clip: text clips the painted background to the glyphs, but it does not resize the gradient to fit them. The gradient still spans the full width and height of the element.
For a single short heading this is invisible. For a multi-line heading, or a heading inside a much wider container, it bites. The gradient stretches across the whole box, so each line of text samples a different slice of it. A two-line title can show the pink end on the first line and the violet end on the second, with neither line showing the full sweep you designed. Inline elements that wrap behave even more unpredictably, because each line fragment clips its own slice of a gradient that was laid out for the entire block.
The fixes are all about constraining the box to the text: display: inline-block with a width: fit-content, or display: table on the heading, so the gradient's coordinate space matches the glyphs it's clipping. None of this is obvious from the three-line snippet, because the snippet works perfectly until the day your heading wraps.
Every one of these traps is downstream of the same fact: gradient text is an effect assembled from parts that were each designed for something else. A clip property borrowed from the box model, a fill color that out-specifies color, a prefix that outlived its decade, and an interpolation default that predates how we think about color now. Getting all four right by hand, every time, is the kind of thing worth letting a tool handle. If you'd rather dial in the colors and the font and copy a snippet that already has the prefixes, the fallback, and the box constraints sorted, the CSS text gradient generator is built for exactly that.