
CSS Text Effects: Gradients, Outlines, and Shadows
Eye-catching headlines used to mean exporting text as an image from a design tool. That meant extra requests, blurry results on high-density screens, text that search engines and screen readers couldn't read, and a trip back to the design tool every time the copy changed. Today, CSS can produce gradient fills, outlined lettering, glows, and layered 3D effects on live, selectable text.
This guide covers the three building blocks, gradient text, text outlines, and text shadows, then combines them into a few effects you can drop into a project. Along the way, I'll point out where each technique can hurt readability and how to keep your styled text accessible.
Gradient Text
The trick behind gradient text is to paint a gradient as the element's background, clip that background to the shape of the text, and make the text itself transparent so the background shows through.
<h1 class="gradient-heading">Ship faster with less CSS</h1>
.gradient-heading {
background-image: linear-gradient(90deg, #38bdf8, #818cf8 50%, #f472b6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
Three properties do the work:
background-imagepaints the gradient behind the element.background-clip: textclips the background so it's only visible inside the glyphs.color: transparentmakes the text fill see-through so the gradient shows.
The unprefixed background-clip: text is supported in current browsers, but including the -webkit- prefixed version as well is still a good idea for slightly older versions. Some authors use -webkit-text-fill-color: transparent instead of color: transparent. That has one advantage: color remains available for things like currentColor-based borders or text decoration.
Make the Gradient Fit the Text
A block-level heading is as wide as its container, so the gradient stretches across the whole line even if the text is short. Only part of the gradient is visible. Making the element shrink to its content fixes that:
.gradient-heading {
display: inline-block; /* or width: fit-content */
background-image: linear-gradient(90deg, #38bdf8, #818cf8, #f472b6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
For headings that wrap onto multiple lines as an inline element, box-decoration-break: clone (with its -webkit- prefix in some browsers) makes each line fragment get the full gradient instead of one gradient split across lines.
Smoother Gradients with Better Color Spaces
Gradients between saturated colors in the default sRGB space can pass through a dull, grayish middle. Interpolating in OKLCH or OKLab keeps them vivid:
.gradient-heading {
background-image: linear-gradient(in oklch 90deg, #0ea5e9, #ec4899);
}
Color space interpolation in gradients is supported in current versions of the major browsers. Add a plain gradient before it as a fallback if you need to support older browsers, since the browser uses the last declaration it understands:
.gradient-heading {
background-image: linear-gradient(90deg, #0ea5e9, #ec4899);
background-image: linear-gradient(in oklch 90deg, #0ea5e9, #ec4899);
}
Animated Gradient Text
Make the background larger than the element and animate its position for a shimmering effect:
.shimmer {
display: inline-block;
background-image: linear-gradient(
100deg,
#818cf8 0%,
#38bdf8 25%,
#f472b6 50%,
#38bdf8 75%,
#818cf8 100%
);
background-size: 200% auto;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: shimmer 4s linear infinite;
}
@keyframes shimmer {
to {
background-position: -200% center;
}
}
@media (prefers-reduced-motion: reduce) {
.shimmer {
animation: none;
}
}
Animating background-position repaints the text every frame, so use it on a single headline, not on paragraphs.
Don't Forget a Fallback Color
If the gradient fails to load or the clip isn't supported, transparent text is invisible. Wrap the effect in a feature query so unsupported browsers get a solid color:
.gradient-heading {
color: #6366f1;
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
.gradient-heading {
background-image: linear-gradient(90deg, #38bdf8, #818cf8, #f472b6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
}
Outlined Text
Using -webkit-text-stroke
Despite the prefix, -webkit-text-stroke works in all major browsers, and it's the simplest way to outline text:
.outline-heading {
font-size: clamp(3rem, 10vw, 7rem);
font-weight: 900;
color: transparent;
-webkit-text-stroke: 2px #0f172a;
}
With a transparent fill you get hollow letters. With a solid fill you get a colored outline around filled text.
The Inner Stroke Problem and paint-order
Text stroke is drawn centered on the glyph outline, half inside the letter and half outside. With a thick stroke on filled text, the inside half eats into the letterforms and thin fonts start to look chunky or even illegible.
paint-order: stroke fill tells the browser to paint the stroke first and the fill on top, so only the outside half of the stroke is visible:
.sticker-text {
font-size: 4rem;
font-weight: 800;
color: #fbbf24;
-webkit-text-stroke: 8px #0f172a;
paint-order: stroke fill;
}
Double the stroke width to get the visible outline thickness you want, since half of it is hidden behind the fill. paint-order on HTML text is supported in recent versions of the major browsers, but support arrived later than for SVG text, so check caniuse if older browsers matter to you. Without it, you just get a centered stroke, which still looks acceptable at thinner widths.
Outlines with text-shadow
Before text-stroke was widely usable, people built outlines out of several offset shadows. It's still handy when you want a soft or rounded outline:
.shadow-outline {
color: #fff;
text-shadow:
-1px -1px 0 #0f172a,
1px -1px 0 #0f172a,
-1px 1px 0 #0f172a,
1px 1px 0 #0f172a;
}
This works well for thin outlines of one or two pixels. Beyond that, gaps appear at the corners and you'd need many more shadows.
Text Shadows
text-shadow takes an x offset, a y offset, an optional blur radius, and a color. Unlike box-shadow, there's no spread value and no inset keyword.
.subtle {
text-shadow: 0 1px 2px rgb(0 0 0 / 0.25);
}
A subtle shadow like this can improve legibility of light text on busy backgrounds, like a caption over a photo. Beyond that, text-shadow becomes a creative tool, especially when you layer several shadows. Shadows are painted in order, with the first one on top.
Neon Glow
Stack several blurred shadows in the same color with increasing blur radii:
.neon {
color: #fdf4ff;
background: #0b0f19;
text-shadow:
0 0 4px #fff,
0 0 10px #f472b6,
0 0 20px #f472b6,
0 0 40px #ec4899,
0 0 80px #ec4899;
}
For a flickering sign, animate the shadow opacity with a few quick keyframes, and turn the animation off for users who prefer reduced motion.
Long Shadow
Repeat a hard shadow with a one-pixel step to create a long, flat shadow:
.long-shadow {
color: #f8fafc;
font-size: 5rem;
font-weight: 900;
text-shadow:
1px 1px 0 #1e293b,
2px 2px 0 #1e293b,
3px 3px 0 #1e293b,
4px 4px 0 #1e293b,
5px 5px 0 #1e293b,
6px 6px 0 #1e293b,
7px 7px 0 #1e293b,
8px 8px 0 #1e293b;
}
If you use this often, generate the shadow list with a Sass loop:
@use "sass:list";
@function long-shadow($length, $color) {
$shadows: ();
@for $i from 1 through $length {
$shadows: list.append(
$shadows,
($i * 1px) ($i * 1px) 0 $color,
$separator: comma
);
}
@return $shadows;
}
.long-shadow {
text-shadow: long-shadow(24, #1e293b);
}
3D Extruded Text
Shift the shadow color gradually darker as it goes deeper, and finish with a soft blurred shadow on the ground:
.extruded {
color: #fbbf24;
font-size: 5rem;
font-weight: 900;
text-shadow:
0 1px 0 #d97706,
0 2px 0 #c2410c,
0 3px 0 #b45309,
0 4px 0 #92400e,
0 5px 0 #78350f,
0 8px 12px rgb(0 0 0 / 0.4);
}
Letterpress and Emboss
A light shadow slightly below and a dark shadow slightly above make text look pressed into the surface:
.letterpress {
color: #64748b;
background: #cbd5e1;
text-shadow:
0 1px 0 rgb(255 255 255 / 0.6),
0 -1px 0 rgb(0 0 0 / 0.15);
}
Swap the offsets to make it look raised instead.
Combining Effects
The real fun is in combinations. There's one limitation to know about: text-shadow is painted above the element's background, and with gradient text the gradient is the background. Add a shadow to gradient text and the shadow covers the gradient inside the letters.
The simplest workaround is filter: drop-shadow(), which shadows the final rendered shape of the element, gradient included:
.gradient-heading.has-glow {
filter: drop-shadow(0 6px 12px rgb(129 140 248 / 0.5));
}
If you need a true text-shadow (for example, several layered shadows), render the text twice: the element itself draws the shadow, and a pseudo-element on top draws the gradient.
<h1 class="gradient-shadow" data-text="Launch Week">Launch Week</h1>
.gradient-shadow {
position: relative;
display: inline-block;
font-size: 5rem;
font-weight: 900;
color: #818cf8; /* close to the gradient, hides antialiasing fringes */
text-shadow:
0 4px 0 #312e81,
0 10px 24px rgb(129 140 248 / 0.55);
}
.gradient-shadow::after {
content: attr(data-text);
position: absolute;
inset: 0;
background-image: linear-gradient(90deg, #38bdf8, #818cf8, #f472b6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
The pseudo-element sits exactly on top of the original text, so the gradient covers the solid letters while the shadows show around them. Keep the data-text attribute in sync with the visible text. Some screen readers announce generated content, which would read the heading twice; in browsers that support alternative text for content, you can write content: attr(data-text) / "" to mark it as decorative.
Either way, drop-shadow and large blurred shadows are more expensive to paint than a simple text-shadow, so avoid animating them.
Keeping Text Readable and Accessible
Text effects are for display type. A few guidelines keep them from getting in the way:
- Check contrast. For gradient text, test contrast at the lightest part of the gradient against the background. Outlined text with a transparent fill needs a thick enough stroke and strong contrast to remain readable.
- Use effects on large text only. Headlines, hero text, and logos, not paragraphs or UI labels.
- Respect reduced motion. Wrap animated effects in
prefers-reduced-motionchecks. - Keep it real text. Don't split words into separate spans per letter unless you must, since that can confuse some screen readers. If you do, put the full text in an
aria-labelon the parent and hide the individual letters witharia-hidden="true". - Test forced colors mode. In Windows High Contrast and similar modes, backgrounds are removed, so gradient text with
color: transparentcan disappear. Restore a real color:
@media (forced-colors: active) {
.gradient-heading,
.shimmer,
.gradient-shadow {
background: none;
color: CanvasText;
-webkit-text-fill-color: CanvasText;
}
}
- Check selection. Selected gradient text can look odd, since the selection highlight may not match. Style
::selectionif it looks broken.
.gradient-heading::selection {
color: #0f172a;
-webkit-text-fill-color: #0f172a;
background: #bae6fd;
}
Performance Notes
Most text effects are cheap for static headlines. Costs add up when you animate them or apply them to lots of text. Large blur radii, many stacked shadows, and animated background-position all require repainting on every frame. Keep animated effects to a single element, and prefer animating opacity or transform on a wrapper where possible.
Conclusion
Gradients, outlines, and shadows cover most of what designers ask for in display typography, and they all work on real, selectable, searchable text. Clip a gradient to text with background-clip: text, outline letters with -webkit-text-stroke and paint-order, and layer text-shadow for glows, long shadows, and 3D effects.
Use them on large type, provide a solid-color fallback and a forced-colors override, and check contrast at the weakest point of the effect. That way your headlines look striking and stay readable for everyone.


