CSS Gradient Text Guide: Background-Clip, Animation & Advanced Tricks
CSS doesn't have a color: gradient value — color only ever accepts a single solid color. But with one clever
trick, you can still paint a gradient across your text and create visually striking typography.
This guide explains how gradient text works, why each line of the trick is needed, and when to use each approach — starting from a basic linear gradient and working up to animated, stroked, blended, and video-filled text, plus the production pitfalls that catch most people out.
If gradients themselves are new to you, start with
Mastering CSS Gradients: Types, Use Cases, and Best Practices — this post
assumes you're comfortable writing a linear-gradient().
How Gradient Text Works
Creating gradient text in CSS follows a simple 4-step process:
- Declare a solid fallback
colorfor browsers that can't clip backgrounds to text - Apply a gradient as the element's background
- Clip the background to the text with
background-clip: text - Make the text's own fill transparent with
-webkit-text-fill-color: transparent, so the clipped gradient shows through
.gradient-text {
color: #ff7e5f; /* 1. Fallback */
background: linear-gradient(90deg, #ff7e5f, #feb47b); /* 2. The gradient */
-webkit-background-clip: text; /* 3. Clip it to the glyphs... */
background-clip: text;
-webkit-text-fill-color: transparent; /* 4. ...and let it show through */
}Visual Illustration
What Each Line Does
color— normally paints the text. Here it's the safety net for browsers that ignore the whole trick: the text still renders in a readable solid color. It can't help a browser that understands the transparent fill but can't clip — the fill still wins there and the text disappears, which is what@supportsguards against.background— the gradient is painted across the element's whole box, exactly like any other background.background-clip: text— throws away every part of that background that isn't underneath a glyph. What's left is a gradient in the exact shape of your letters.-webkit-text-fill-color: transparent— the text is still painted on top of its background, so it would cover the gradient completely. Making the fill transparent removes that top layer.
Essential Techniques
With the four-line recipe in place, each "flavor" of gradient text is mostly a matter of swapping the background.
Linear Gradient Text
The industry standard for production — a straight blend from one color to another.
.linear-gradient {
color: #fda085; /* Fallback */
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}When to use: blog headings, marketing pages, simple static gradient effects.
Radial & Conic Gradients
Swap the gradient function and nothing else changes. Radial gradients radiate out from a focal point, while conic gradients sweep around a center like a color wheel — best saved for decorative, experimental UI.
.radial-gradient {
background: radial-gradient(circle at 30% 40%, #fff7ad, #ff6a00 70%);
}
.conic-gradient {
background: conic-gradient(from 90deg, red, yellow, lime, cyan, blue, magenta, red);
}
/* Both share the same clipping recipe */
.radial-gradient,
.conic-gradient {
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}Two details matter for conic text:
- End on the first color. The sweep's last stop meets its first at the start angle, so if they differ you get a
hard seam running through the letters. Repeating the first color (
redat both ends here) closes the circle smoothly. from <angle>rotates the sweep. It moves where the first color starts (0degis straight up, turning clockwise), which decides which letters get which hues.
When to use: decorative headings, experimental UI designs, visual demos and showcases.
For how the focal point and sweep angle work, see the radial and conic sections of the gradients guide.
Animated Gradient Text
Perfect for high-impact hero sections. Browsers can't smoothly animate between two gradient images, so instead we make the background wider than the text and slide it along.
.animated-gradient {
color: #ff6a00; /* Fallback */
background: linear-gradient(90deg, #ff6a00, #ee0979, #00c6ff, #ff6a00);
background-size: 200% auto;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: shine 3s linear infinite;
}
@keyframes shine {
to {
background-position: 200% center;
}
}Visual Illustration
Three details make this loop seamless:
background-size: 200% automakes the gradient twice as wide as the text, so only half of it is visible at any moment — the animation pans that visible window along the strip.- The first and last color stops match (
#ff6a00at both ends). When the animation jumps back to the start, the colors line up exactly and the reset is invisible. - The gradient runs at
90deg. The keyframes slide the strip sideways, and a tilted gradient's left and right edges don't match, so any other angle shows a seam every time the pattern repeats. To animate a gradient at any angle, use@propertyinstead.
When to use: hero titles, highlighted callouts, attention-grabbing UI elements.
For a refresher on @keyframes and the animation shorthand, see the
CSS keyframes guide.
Gradient Stroke (Outline) Text
Instead of filling the letters, this technique paints the gradient only on their outline — great for logos and futuristic "neon" aesthetics.
The trick is that background-clip: text clips to the whole glyph, including its stroke. So a transparent
stroke widens the clipped area, and the fill is set to the background color behind the text to cover the
middle — leaving only the stroke ring showing the gradient.
.stroke-gradient {
background: linear-gradient(90deg, #00dbde, #fc00ff);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-stroke: 3px transparent;
-webkit-text-fill-color: #0f1220; /* Must match the background behind the text */
}Visual Illustration
Best use cases: logos, large display text, neon or futuristic designs.
Advanced Techniques
Once the core recipe is second nature, you can combine it with shadows, blend modes, media, and scroll effects.
Adding a Shadow
text-shadow doesn't play nicely with gradient text: the shadow is painted as part of the text layer, which sits
above the clipped background, so it shows through the transparent fill and muddies the gradient.
The simplest fix is filter: drop-shadow(), which shadows the element's final rendered pixels — the gradient
letters themselves:
.shadow-gradient {
background: linear-gradient(to bottom, #fff, #64748b);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
filter: drop-shadow(4px 4px 2px rgba(0, 0, 0, 0.5));
}If you need a real text-shadow (for example, several stacked shadows for a retro effect), split the effect into
two layers instead: the element draws the shadow, and a pseudo-element duplicates the text on top with the
gradient.
<h1 class="layered-gradient" data-text="Layered">Layered</h1>.layered-gradient {
position: relative;
color: transparent;
text-shadow: 3px 3px 0 #1e293b, 6px 6px 0 #64748b;
}
.layered-gradient::after {
content: attr(data-text);
content: attr(data-text) / ""; /* Empty alt text: hide the duplicate from screen readers */
position: absolute;
inset: 0;
background: linear-gradient(to bottom, #fff, #64748b);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: none;
}When to use: hero headings that need depth, retro or 3D-style display text.
Blend Modes
mix-blend-mode blends the element's pixels with whatever sits behind it. Combined with the clipping recipe, the
gradient letters pick up the texture or photo underneath them.
.blend-container {
background: url("carbon-fibre-texture.png");
padding: 10px;
}
.blend-gradient {
color: #00ffcc; /* Fallback */
background: linear-gradient(to right, #00ffcc, #ff0055);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
mix-blend-mode: color-dodge;
}Which mode to reach for depends on the backdrop:
screenonly ever lightens, so the gradient shows clearly over a dark texture and washes out over a light one.color-dodgebrightens the backdrop's mid-tones into a glow, but it leaves pure black black — so the letters need something other than solid black behind them.hard-lightkeeps the gradient's own light and dark areas and lays the texture over them;overlayis the reverse, keeping the texture's light and dark areas and tinting them with the gradient.differenceinverts whatever is behind the letters — striking, but the colors are hard to predict.luminositykeeps the gradient's brightness and takes its hue from the backdrop, so the text mostly loses the gradient's colors.
Image, GIF & Video Fills
Since background-clip: text clips any background, you aren't limited to gradients — an image or animated GIF
works the same way:
.image-text {
background: url("gradient-background.gif") center / cover;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}A <video> can't be used as a CSS background, so background-clip can't reach it. Instead, place the text over
the video and let a blend mode "knock out" the letters. With mix-blend-mode: screen, white areas stay white and
black areas become fully transparent — so black text on a white box turns into windows onto the video:
<div class="video-text">
<video autoplay muted loop playsinline>
<source src="/assets/videos/gradient.webm" type="video/webm">
<source src="/assets/videos/gradient.mp4" type="video/mp4">
</video>
<h1>Gradient Text</h1>
</div>.video-text {
position: relative;
}
.video-text video {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.video-text h1 {
position: absolute;
inset: 0;
display: grid;
place-items: center;
margin: 0;
background: #fff; /* Stays white after blending */
color: #000; /* Becomes a see-through window onto the video */
mix-blend-mode: screen;
}The white box stays visible around the letters, so this works best when the surrounding page is also white (use
multiply with white text on a black box for dark pages).
Best for: landing pages, creative portfolios, experimental typography.
The "Window" Effect (Fixed Attachment)
A subtle, high-end effect often seen on premium landing pages. With background-attachment: fixed, the gradient
is pinned to the viewport rather than the text. As the user scrolls, the text acts like a moving window,
revealing different parts of one large, static gradient.
.window-gradient {
background: linear-gradient(45deg, #00f2fe 0%, #4facfe 50%, #f093fb 100%);
background-attachment: fixed; /* Pins the gradient to the viewport */
background-size: cover; /* Stretches it across the whole screen */
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}Visual Illustration
Why it's a hidden gem:
- Depth: it creates a sense of parallax motion without any JavaScript.
- Consistency: multiple gradient headings on the same page all look like they're cut from one giant background.
Best use cases: long-form storytelling pages, portfolio sections with large bold headings, a "reveal" effect as the user scrolls.
Animating Colors and Angles with @property
The background-position trick only pans a gradient; it can't change its colors or rotate it. Normally a gradient
can't be animated at all, because a custom property like --angle is just an untyped string to the browser.
Registering it with @property gives it a type, and a typed property can be interpolated:
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.rotating-gradient {
background: linear-gradient(var(--angle), #ff6a00, #ee0979, #00c6ff);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: rotate-gradient 4s linear infinite;
}
@keyframes rotate-gradient {
to {
--angle: 360deg;
}
}The same idea works for colors. Register each stop as a <color> property, then animate the two stops trading
places — the gradient stays put while its hues shift in place, instead of panning like the background-position
version:
@property --stop-a {
syntax: "<color>";
inherits: false;
initial-value: #ff6a00;
}
@property --stop-b {
syntax: "<color>";
inherits: false;
initial-value: #ee0979;
}
.shifting-gradient {
color: #ff6a00; /* Fallback */
background: linear-gradient(90deg, var(--stop-a), var(--stop-b));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: shift-hues 4s ease-in-out infinite;
}
@keyframes shift-hues {
0%,
100% {
--stop-a: #ff6a00;
--stop-b: #ee0979;
}
50% {
--stop-a: #ee0979;
--stop-b: #ff6a00;
}
}Because the stops are animated rather than the image's position, this version works at any gradient angle.
Responsive & Themeable Gradient Text
Fluid Sizing
Gradients scale naturally with text, so a fluid font-size is all you need for the effect to look right on every
screen:
.responsive-gradient {
font-size: clamp(2rem, 8vw, 5rem);
line-height: 1.2;
overflow-wrap: break-word;
background: linear-gradient(90deg, #ff6a00, #ee0979);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}overflow-wrap: break-word stops a long word from overflowing on narrow screens, and a line-height of at least
1.2 keeps descenders inside the painted background (see
Clipped Descenders and Italic Overhangs).
Dynamic Gradients with CSS Variables
CSS variables let you change a gradient per theme, per component, or at runtime:
:root {
--gradient-start: #c2410c;
--gradient-end: #be185d;
}
/* Brighter stops for dark backgrounds, so contrast stays high */
@media (prefers-color-scheme: dark) {
:root {
--gradient-start: #fb923c;
--gradient-end: #f472b6;
}
}
.variable-gradient {
color: var(--gradient-start); /* Fallback */
background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}Combined with JavaScript (element.style.setProperty("--gradient-start", "#22c55e")), this enables live gradient
updates — exactly the trick the playground further down this post uses to update colors
as you drag its controls.
Production Pitfalls and Fixes
Applying a gradient is easy; making it hold up across browsers, layouts, and edge cases is the hard part.
The Multi-Line Wrap Fix
The problem: when gradient text wraps onto several lines, the lines don't each get the full gradient. On a
block element (like an <h1>), the gradient is painted once across the whole box, so each line only gets whatever
slice of it happens to sit behind it. On an inline element (like a <span>), the line fragments are treated as one
long strip laid end to end, so the first line gets the start of the gradient and the last line gets the end.
The solution: put the gradient on an inline element and add box-decoration-break: clone. This tells the
browser to paint the background separately for each line fragment, so every line gets the complete gradient.
Visual Illustration
<h1><span class="wrap-fix">Gradient text that wraps across several lines</span></h1>.wrap-fix {
background: linear-gradient(90deg, #ff6a00, #ee0979);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}You can see this exact toggle live in the playground below — force the demo text to wrap, then flip the fix on and off to watch each line switch between a slice of the gradient and the full thing.
Clipped Descenders and Italic Overhangs
The gradient only covers the element's box, but glyphs don't always stay inside it. With a tight line-height,
descenders (the tails of g, j, p, q, y) hang below the box. Italic and script fonts often lean past its right edge.
Any part of a glyph outside the box has no background behind it, so with a transparent fill it simply
disappears.
Visual Illustration
.gradient-text {
line-height: 1.2; /* Enough room for descenders */
padding-block-end: 0.1em; /* Extra room for deep descenders */
padding-inline-end: 0.1em; /* Room for italic overhang */
}The background Shorthand Resets background-clip
The background shorthand resets every background sub-property it doesn't mention — including
background-clip. So a later rule that "just changes the gradient" silently switches clipping back to
border-box, and your text vanishes behind a solid gradient rectangle.
.gradient-text {
background: linear-gradient(90deg, #ff6a00, #ee0979);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* ❌ Resets background-clip — the text disappears */
.gradient-text:hover {
background: linear-gradient(90deg, #00c6ff, #ee0979);
}
/* ✅ Only replaces the image, so clipping survives */
.gradient-text:hover {
background-image: linear-gradient(90deg, #00c6ff, #ee0979);
}When overriding a gradient, reach for background-image instead of background.
Feature Detection with @supports
A fallback color covers most cases, but the most robust approach is to only apply the transparent fill when
clipping is actually supported. Then a browser that can't clip never ends up with invisible text:
h1 {
color: #6366f1; /* Everyone gets a readable solid color */
}
@supports (-webkit-background-clip: text) or (background-clip: text) {
h1 {
background: linear-gradient(to right, #6366f1, #ec4899);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
}The SVG Fail-Safe
For high-reliability branding (like a company logo), SVG is the most dependable option. The gradient is part of the graphic itself, so it doesn't depend on background clipping at all and renders identically across browsers.
<svg viewBox="0 0 400 60" role="img" aria-label="SVG Gradient Text">
<defs>
<linearGradient id="svgGrad" x1="0%" y1="50%" x2="100%" y2="50%">
<stop offset="0%" stop-color="#ff7e5f" />
<stop offset="100%" stop-color="#feb47b" />
</linearGradient>
</defs>
<text x="0" y="45" fill="url(#svgGrad)" font-size="40" font-weight="800">SVG Gradient Text</text>
</svg>SVG gradients don't take a CSS angle. Their direction is a line from (x1, y1) to (x2, y2), measured across
the text's bounding box — so 0%, 50% → 100%, 50% runs left to right, the same as 90deg, and
0%, 100% → 100%, 0% runs bottom-left to top-right, close to 45deg. That's the default direction, but spelling
it out makes it easy to change.
The trade-off: SVG <text> doesn't wrap onto new lines, and it's harder to style from your main stylesheet — so
keep it for short, fixed pieces of text. If SVG is new to you, the
SVG essentials guide is a good starting point.
Choosing a Technique
| Technique | Performance | Browser Support | Best Use Case | Complexity |
|---|---|---|---|---|
| Linear gradient | Excellent | Excellent | Headings, UI highlights | Easy |
| Radial/conic | Excellent | Excellent | Decorative UI | Easy |
| Animated gradient | Good | Excellent | Hero sections | Medium |
| Stroke gradient | Good | Good | Logos, display text | Medium |
| Shadow layering | Good | Excellent | Headings with depth | Medium |
| Blend mode | Fair | Good | Creative designs | Hard |
| Image/GIF/video | Fair | Good | Portfolios, landing pages | Medium |
@property animation | Good | Good | Rotating or shifting hues | Medium |
| SVG gradient | Excellent | Excellent | Logos, production assets | Medium |
When in doubt, start with a linear gradient and the @supports fallback — it covers the vast majority of real
use cases.
Accessibility and Performance
Check Contrast at Every Stop
A gradient's contrast isn't one number. Every color stop has to stand out against the background, and the lightest stop (on a light page) or the darkest stop (on a dark page) is the one that fails first. Check each stop with a contrast checker and aim for at least WCAG AA: 4.5:1 for body-size text, or 3:1 for large text.
Respect Reduced Motion
Some visitors turn on their operating system's "reduce motion" setting because constant movement causes them discomfort. Stopping an animated gradient for them costs one media query:
@media (prefers-reduced-motion: reduce) {
.animated-gradient,
.rotating-gradient,
.shifting-gradient {
animation: none;
}
}Include every animated variant — the @property versions move just as much as the panning one.
The keyframes guide covers this in more depth in Respecting Motion Preferences.
Plan for Forced Colors Mode
In Windows High Contrast (forced colors) mode, the browser replaces your colors with the user's chosen system palette. Depending on how a browser applies that, a transparent fill over a background image can end up invisible or low-contrast. An explicit reset makes the outcome predictable:
@media (forced-colors: active) {
.gradient-text {
background: none;
-webkit-text-fill-color: currentColor;
}
}Keep It Real Text
Gradient text is still real text — it can be selected, translated, found with Ctrl+F, and read by screen readers. That's a big advantage over baking the effect into an image, so keep the words in the HTML rather than in a PNG.
Performance Considerations
- Static gradient text is cheap. It's painted once, like any other background.
- Animated gradients repaint on every frame. Animating
background-position(or a@propertyvalue) can't be handed off to the GPU the waytransformandopacitycan, so the text is repainted continuously. Keep animations to short headings, never long blocks of text. will-changewon't make it free.will-change: background-positioncan move the element onto its own layer so each repaint touches less of the page, but the text still repaints every frame. Use it sparingly, and only on elements that are actually animating.
For why some properties are cheap to animate and others aren't, see
Why GPU-Accelerated Properties Are Cheap
and The will-change Property.
When Not to Use Gradient Text
While gradient text is visually appealing, it isn't always the right choice. Avoid it for:
- Long paragraphs: gradients reduce readability and cause eye strain over long runs of text.
- Low-contrast backgrounds: some part of the gradient will almost certainly fail contrast.
- Critical UI elements: buttons, forms, and navigation should stay instantly readable.
- Performance-sensitive pages: skip heavy animations on pages aimed at low-end devices.
- Formal or professional interfaces: overuse can feel distracting.
Real-World Example: Gradient Text in My Hero Section
The techniques in this post aren't just demos — they're used in production in this blog's homepage hero section.
If you visit the homepage, you'll see an animated gradient applied to the main heading:
<section class="hero">
<div class="hero-content">
<h1 class="hero-heading">Hi, I'm Harshal LADHE 👋</h1>
<div class="hero-subheading">A showcase of my technical blog posts, coding insights, and open source projects.</div>
<p class="hero-typed">
I'm ...
</p>
</div>
</section>Animated Gradient on the Main Heading
The .hero-heading uses a flowing linear gradient with the background animation from
Animated Gradient Text:
.hero-heading {
background: linear-gradient(
90deg,
var(--color-fg-attention),
var(--color-fg-success),
var(--color-fg-danger),
var(--color-fg-accent),
var(--color-fg-attention)
);
background-size: 200% auto;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
animation: bg-slide 3s linear infinite;
}Notice that the first and last stops are both --color-fg-attention, which is what makes the loop seamless — a
smooth sliding gradient that makes a strong first impression.
Gradient Highlight for Typed Roles
The dynamically typed roles use a separate gradient treatment:
.hero-highlight {
background-image: linear-gradient(
45deg,
var(--color-fg-danger) 20%,
var(--color-fg-success) 45%,
var(--color-fg-attention) 70%
);
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
}This version uses the site's color variables so it follows the light and dark themes, skips animation (the typing effect already provides motion), and keeps contrast strong for readability.
Interactive Playground
Try every technique from this post in one live example:
- Essentials: linear, radial, conic, animated, and stroke gradient text.
- Advanced: shadows (
drop-shadow(), the layeredtext-shadowtrick, and the broken plaintext-shadowfor comparison), blend modes over a texture, image and animated-image fills, the video knockout, the fixed "window" effect, and@propertyrotation and color shifting. - Pitfalls & Fixes: the multi-line wrap fix, clipped descenders, the
backgroundshorthand reset, an@supportsguard tested against a simulated browser that can't clip, and the SVG fail-safe.
Drag the angle and color controls to see CSS variables update the gradient in real time. The chips under the preview check each color stop's contrast, and the panel below always shows the CSS for exactly what you're looking at. The Production CSS options can add CSS variables, fluid sizing, and a forced-colors reset before you copy it.
Gradient Text Playground
Switch between linear, animated, and stroke gradient text, tune the angle and colors live, and toggle the multi-line wrap fix to see box-decoration-break in action.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Gradient Text Playground</title> </head> <body> <main class="playground"> <header class="playground-header"> <h1>Gradient Text Playground</h1> <p>Every technique from the post in one place — pick one, tune the angle and colors live, and copy the CSS for exactly what you see.</p> </header> <section class="demo-area" aria-label="Gradient text preview"> <!-- Most techniques: one clipped heading. --> <div class="stage" id="text-stage"> <h2 class="text-demo"><span class="gradient-demo" id="gradient-demo">Gradient Text</span></h2> </div> <!-- Window effect: several headings in a scroll box, all cut from one viewport-pinned gradient. --> <div class="stage" id="window-stage" hidden> <div class="window-scroll" tabindex="0" aria-label="Scroll to move the headings across the fixed gradient"> <p>Scroll this box ↓</p> <h2 class="text-demo"><span class="gradient-demo mode-window">Window One</span></h2> <h2 class="text-demo"><span class="gradient-demo mode-window">Window Two</span></h2> <h2 class="text-demo"><span class="gradient-demo mode-window">Window Three</span></h2> <h2 class="text-demo"><span class="gradient-demo mode-window">Window Four</span></h2> </div> </div> <!-- Video knockout: the text sits over a real <video>, fed here by a canvas so no file is needed. --> <div class="stage" id="video-stage" hidden> <div class="video-text"> <video id="knockout-video" autoplay muted loop playsinline></video> <h2>Gradient</h2> </div> </div> <!-- SVG fail-safe: the gradient is part of the graphic, no background clipping involved. --> <div class="stage" id="svg-stage" hidden> <svg class="svg-demo" viewBox="0 0 400 60" role="img" aria-label="SVG Gradient Text"> <defs> <linearGradient id="svg-grad" gradientUnits="objectBoundingBox"> <stop offset="0%" id="svg-stop-1" stop-color="#ff6a00" /> <stop offset="100%" id="svg-stop-2" stop-color="#ee0979" /> </linearGradient> </defs> <text x="200" y="45" text-anchor="middle" fill="url(#svg-grad)" font-size="40" font-weight="800" font-family="system-ui, sans-serif">SVG Gradient Text</text> </svg> </div> </section> <p class="stage-note" id="stage-note"></p> <ul class="contrast" id="contrast" aria-label="Contrast of each color stop against the preview background"></ul> <output id="gradient-text-output" class="readout" aria-live="polite"></output> <div class="tabs" role="tablist" aria-label="Technique group" id="group-switch"> <button class="tab tab--active" type="button" data-mode="essentials" role="tab" aria-selected="true">Essentials</button> <button class="tab" type="button" data-mode="advanced" role="tab" aria-selected="false">Advanced</button> <button class="tab" type="button" data-mode="pitfalls" role="tab" aria-selected="false">Pitfalls & Fixes</button> </div> <div id="essentials-demo" class="tab-panel tab-panel--active"> <fieldset class="control-group"> <legend>Technique</legend> <div class="segmented-control" role="radiogroup" aria-label="Essential technique"> <label class="segment"><input type="radio" name="technique" value="linear" checked><span>Linear</span></label> <label class="segment"><input type="radio" name="technique" value="radial"><span>Radial</span></label> <label class="segment"><input type="radio" name="technique" value="conic"><span>Conic</span></label> <label class="segment"><input type="radio" name="technique" value="animated"><span>Animated</span></label> <label class="segment"><input type="radio" name="technique" value="stroke"><span>Stroke</span></label> </div> </fieldset> </div> <div id="advanced-demo" class="tab-panel"> <fieldset class="control-group"> <legend>Technique</legend> <div class="segmented-control" role="radiogroup" aria-label="Advanced technique"> <label class="segment"><input type="radio" name="technique" value="shadow"><span>Shadow</span></label> <label class="segment"><input type="radio" name="technique" value="blend"><span>Blend</span></label> <label class="segment"><input type="radio" name="technique" value="image"><span>Image</span></label> <label class="segment"><input type="radio" name="technique" value="video"><span>Video</span></label> <label class="segment"><input type="radio" name="technique" value="window"><span>Window</span></label> <label class="segment"><input type="radio" name="technique" value="property"><span>@property</span></label> </div> </fieldset> </div> <div id="pitfalls-demo" class="tab-panel"> <fieldset class="control-group"> <legend>Pitfall</legend> <div class="segmented-control" role="radiogroup" aria-label="Pitfall"> <label class="segment"><input type="radio" name="technique" value="wrap"><span>Multi-line</span></label> <label class="segment"><input type="radio" name="technique" value="descenders"><span>Descenders</span></label> <label class="segment"><input type="radio" name="technique" value="shorthand"><span>Shorthand</span></label> <label class="segment"><input type="radio" name="technique" value="supports"><span>@supports</span></label> <label class="segment"><input type="radio" name="technique" value="svg"><span>SVG</span></label> </div> </fieldset> </div> <fieldset class="control-group" id="options-group"> <legend>Options</legend> <div class="segmented-control" id="shadow-variant" role="radiogroup" aria-label="Shadow approach" hidden> <label class="segment"><input type="radio" name="shadow-variant" value="drop-shadow" checked><span>drop-shadow()</span></label> <label class="segment"><input type="radio" name="shadow-variant" value="layered"><span>Layered</span></label> <label class="segment"><input type="radio" name="shadow-variant" value="text-shadow"><span>text-shadow ✗</span></label> </div> <div class="segmented-control" id="image-variant" role="radiogroup" aria-label="Image type" hidden> <label class="segment"><input type="radio" name="image-variant" value="static" checked><span>Static image</span></label> <label class="segment"><input type="radio" name="image-variant" value="animated"><span>Animated (GIF-style)</span></label> </div> <div class="segmented-control" id="property-variant" role="radiogroup" aria-label="Animated property" hidden> <label class="segment"><input type="radio" name="property-variant" value="rotate" checked><span>Rotate angle</span></label> <label class="segment"><input type="radio" name="property-variant" value="hues"><span>Shift colors</span></label> </div> <div class="segmented-control" id="shorthand-variant" role="radiogroup" aria-label="Hover override" hidden> <label class="segment"><input type="radio" name="shorthand-variant" value="image" checked><span>background-image ✓</span></label> <label class="segment"><input type="radio" name="shorthand-variant" value="shorthand"><span>background ✗</span></label> </div> <div class="segmented-control" id="supports-variant" role="radiogroup" aria-label="Simulated browser" hidden> <label class="segment"><input type="radio" name="supports-variant" value="supported" checked><span>Modern browser</span></label> <label class="segment"><input type="radio" name="supports-variant" value="unsupported"><span>No background-clip: text</span></label> </div> <div class="fields-grid"> <div class="field field--full" id="angle-field"> <label for="angle-input" id="angle-label">Angle</label> <div class="range-row"> <span class="range-edge">0deg</span> <div class="range-track"> <input type="range" id="angle-input" min="0" max="360" step="1" value="90"> <output class="range-bubble" id="angle-value" for="angle-input">90deg</output> </div> <span class="range-edge">360deg</span> </div> </div> <div class="field" id="focal-x-field" hidden> <label for="focal-x-input">Focal point X</label> <div class="range-row"> <span class="range-edge">0%</span> <div class="range-track"> <input type="range" id="focal-x-input" min="0" max="100" step="1" value="30"> <output class="range-bubble" id="focal-x-value" for="focal-x-input">30%</output> </div> <span class="range-edge">100%</span> </div> </div> <div class="field" id="focal-y-field" hidden> <label for="focal-y-input">Focal point Y</label> <div class="range-row"> <span class="range-edge">0%</span> <div class="range-track"> <input type="range" id="focal-y-input" min="0" max="100" step="1" value="40"> <output class="range-bubble" id="focal-y-value" for="focal-y-input">40%</output> </div> <span class="range-edge">100%</span> </div> </div> <div class="field"> <label for="color-1">Color 1</label> <input type="color" id="color-1" value="#ff6a00"> </div> <div class="field"> <label for="color-2">Color 2</label> <input type="color" id="color-2" value="#ee0979"> </div> <div class="field field--full" id="stroke-width-field" hidden> <label for="stroke-width-input">Stroke width</label> <div class="range-row"> <span class="range-edge">1px</span> <div class="range-track"> <input type="range" id="stroke-width-input" min="1" max="6" step="1" value="2"> <output class="range-bubble" id="stroke-width-value" for="stroke-width-input">2px</output> </div> <span class="range-edge">6px</span> </div> </div> <div class="field field--full" id="shadow-offset-field" hidden> <label for="shadow-offset-input">Shadow offset</label> <div class="range-row"> <span class="range-edge">1px</span> <div class="range-track"> <input type="range" id="shadow-offset-input" min="1" max="8" step="1" value="4"> <output class="range-bubble" id="shadow-offset-value" for="shadow-offset-input">4px</output> </div> <span class="range-edge">8px</span> </div> </div> <div class="field field--full" id="blend-mode-field" hidden> <label for="blend-mode-select">mix-blend-mode</label> <select id="blend-mode-select"> <option value="color-dodge" selected>color-dodge</option> <option value="screen">screen</option> <option value="overlay">overlay</option> <option value="hard-light">hard-light</option> <option value="difference">difference</option> <option value="luminosity">luminosity</option> </select> </div> </div> <label class="checkbox-field" for="multiline-toggle" id="multiline-field" hidden> <input type="checkbox" id="multiline-toggle" checked> <span>Force multi-line demo text</span> </label> <label class="checkbox-field" for="wrapfix-toggle" id="wrapfix-field" hidden> <input type="checkbox" id="wrapfix-toggle"> <span>Apply <code>box-decoration-break: clone</code></span> </label> <label class="checkbox-field" for="descender-fix-toggle" id="descender-fix-field" hidden> <input type="checkbox" id="descender-fix-toggle"> <span>Add <code>line-height</code> + padding fix</span> </label> <label class="checkbox-field" for="hover-toggle" id="hover-field" hidden> <input type="checkbox" id="hover-toggle" checked> <span>Hold the <code>:hover</code> state on</span> </label> <label class="checkbox-field" for="guard-toggle" id="guard-field" hidden> <input type="checkbox" id="guard-toggle"> <span>Guard the gradient with <code>@supports</code></span> </label> </fieldset> <fieldset class="control-group" id="output-group"> <legend>Production CSS</legend> <label class="checkbox-field" for="vars-toggle"> <input type="checkbox" id="vars-toggle"> <span>Use CSS variables for the color stops</span> </label> <label class="checkbox-field" for="fluid-toggle"> <input type="checkbox" id="fluid-toggle"> <span>Fluid <code>font-size</code> with <code>clamp()</code></span> </label> <label class="checkbox-field" for="forced-toggle"> <input type="checkbox" id="forced-toggle"> <span>Add a <code>forced-colors</code> reset</span> </label> <p class="tip">Animated techniques always get a <code>prefers-reduced-motion</code> rule. The chips above check each color stop against the preview background (WCAG AA for large text needs 3:1).</p> </fieldset> </main> <script src="./index.js"></script> </body> </html>
Starting sandbox…
No console output yet.
No original version of /index.html to compare against.
Frequently Asked Questions
Conclusion
Mastering gradient text isn't about a single CSS property; it's about understanding how backgrounds, clipping, and
fallbacks layer together. Combine background-clip: text with CSS variables, clamp(), @supports, and a
reduced-motion guard, and your typography will be both visually striking and resilient.
Start with a simple linear gradient, and only add motion, strokes, or blend modes when they genuinely improve the experience.
If you want the same layering trick applied to borders instead of text, the companion post
CSS Gradient Borders builds on the same background-clip idea with
padding-box and border-box layers.
The four-line recipe was the easy part — the real work was proving every variant actually renders the way the prose claims. The stroke, blend-mode, and video examples all had to be rebuilt after testing them in a real browser, and the playground's wrap-fix toggle only started doing anything once the gradient moved onto an inline span the flex layout couldn't blockify. I hope flipping that toggle, and watching the readout print the exact CSS behind each change, makes the whole clipping trick click. Thanks for reading all the way to the last gradient stop. 🌈