Text Outline and Stroke Effects
The Problem
Text outline effects look simple. The rendering details are not simple.
A centered stroke cuts into glyph interiors. This is visible on Latin counters like O, P, and R. It is more visible on dense CJK shapes such as 縁 and 取. Thick fake outlines built from text-shadow also degrade fast. Corners turn stepped. Curves turn puffy.
Common requirements for outlined text:
- CSS-first implementation
- Readable on dark surfaces
- Stable for Japanese and English
- Clear tradeoffs for browser support and outline thickness
The Solution
Use the technique that matches the outline thickness and rendering requirement.
- Use
-webkit-text-strokewithpaint-order: stroke fillfor standard UI text and moderate outlines. - Use
text-shadowonly as a fallback for very thin (1px) outlines. - Use SVG
<text>withstrokefor the cleanest browser-native outline quality at any thickness. - Use SVG
feMorphologyonly when you need a true outside-only outline.
Code Examples
-webkit-text-stroke + paint-order
/* Without paint-order — stroke eats glyph interiors */
.text-outline {
color: hsl(48 100% 68%);
-webkit-text-stroke: 6px hsl(336 80% 58%);
}
/* With paint-order — fill covers inward half of stroke */
.text-outline-improved {
color: hsl(48 100% 68%);
-webkit-text-stroke: 6px hsl(336 80% 58%);
paint-order: stroke fill;
}
-webkit-text-stroke draws a centered stroke. Half goes inward, half goes outward. The inward half reduces counters and inner gaps — especially obvious on heavy weights and CJK text.
paint-order: stroke fill paints the stroke first, then the fill on top. The fill covers the inward half. The visible result looks closer to an outside stroke, even though the geometry is still centered.
CJK note: Japanese glyphs have dense interior structure. Thick centered strokes close small gaps earlier than Latin text. Always add paint-order: stroke fill when using -webkit-text-stroke with CJK text.
Browser support: -webkit-text-stroke is Baseline since April 2017. CSS paint-order reached Baseline in March 2024.
text-shadow Outline Hack
/* 4 directions — diamond shape, not enough */
.outline-4 {
text-shadow:
-1px 0 0 hsl(338 80% 56%),
1px 0 0 hsl(338 80% 56%),
0 -1px 0 hsl(338 80% 56%),
0 1px 0 hsl(338 80% 56%);
}
/* 8 directions — minimum usable 1px outline */
.outline-8 {
text-shadow:
-1px -1px 0 hsl(338 80% 56%),
0 -1px 0 hsl(338 80% 56%),
1px -1px 0 hsl(338 80% 56%),
-1px 0 0 hsl(338 80% 56%),
1px 0 0 hsl(338 80% 56%),
-1px 1px 0 hsl(338 80% 56%),
0 1px 0 hsl(338 80% 56%),
1px 1px 0 hsl(338 80% 56%);
}
Programmatic generation for thicker outlines:
function createOutlineShadows(radius, color) {
const shadows = [];
for (let y = -radius; y <= radius; y += 1) {
for (let x = -radius; x <= radius; x += 1) {
if (x === 0 && y === 0) continue;
if (Math.hypot(x, y) <= radius) {
shadows.push(`${x}px ${y}px 0 ${color}`);
}
}
}
return shadows.join(",\n");
}
text-shadow does not draw a real stroke. It stacks offset copies of the text. Four directions produce a diamond. Eight directions are the minimum usable 1px ring. Thick outlines require many shadow entries and still render poorly — stepped and puffy.
CJK note: Dense kanji shapes expose the weakness earlier. Interior details blur together faster than Latin text. Keep this technique at 1px maximum.
SVG Text with stroke + paint-order
<svg viewBox="0 0 920 260" xmlns="http://www.w3.org/2000/svg">
<text
x="460" y="112"
text-anchor="middle"
font-size="68" font-weight="900"
font-family="system-ui, sans-serif"
fill="hsl(54 100% 70%)"
stroke="hsl(338 80% 58%)"
stroke-width="10"
stroke-linejoin="round"
paint-order="stroke fill">
縁取りテキスト
</text>
</svg>
SVG text gives the cleanest browser-native outline quality. You control stroke, stroke-width, stroke-linejoin, and paint-order directly on the text element. Rendering stays sharper than text-shadow, especially on thick outlines.
Set stroke-linejoin="round" for heavy outlines. Round joins prevent sharp corner spikes and keep dense curves smoother.
CJK note: This is the strongest browser-native option for bold Japanese display text when thick outlines and clean corners are needed.
SVG feMorphology Filter
<filter id="outside-outline" x="-15%" y="-25%" width="130%" height="150%">
<feMorphology in="SourceAlpha" operator="dilate" radius="6" result="dilated" />
<feFlood flood-color="hsl(338 80% 58%)" result="outlineColor" />
<feComposite in="outlineColor" in2="dilated" operator="in" result="outlineFill" />
<feComposite in="outlineFill" in2="SourceAlpha" operator="out" result="outsideOnly" />
<feMerge>
<feMergeNode in="outsideOnly" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
This filter dilates the source alpha, colors the larger silhouette, removes the original alpha from that colored shape, and merges the original text back on top. The result is a purely outside outline.
This is the only technique in this article that creates a true outside-only stroke. It is useful when inward stroke intrusion is unacceptable.
The filter region must be enlarged with x, y, width, and height attributes. If you keep the default region, the outline gets clipped.
CJK note: This preserves interior counters completely. Test filter performance and clipping on complex layouts.
Comparison
Quick Reference
| Technique | Thick Outlines (3-5px+) | Edge Quality | CJK Compatibility | Use Case |
|---|---|---|---|---|
-webkit-text-stroke + paint-order | Good | Good | Good with paint-order | General UI text and headings |
text-shadow outline hack | Poor | Low | Fair for thin outlines only | Thin fallback outlines |
SVG <text> with stroke | Excellent | Excellent | Excellent | Display text and precise rendering |
SVG feMorphology filter | Excellent | Excellent | Excellent | True outside-only outline |
Common AI Mistakes
- Using
-webkit-text-strokewithoutpaint-order— the stroke eats glyph interiors, especially on CJK text where dense counters close up - Using only 4
text-shadowvalues — the result is a diamond shape, not a circular outline. Eight directions are the minimum for 1px - Applying thick
text-shadowoutlines — thick outlines need 20+ shadows and still render poorly with stepped, puffy edges - Forgetting
stroke-linejoin="round"on SVG text — heavy outlines on curves and CJK corners produce sharp spikes with the defaultmiterjoin - Not enlarging the SVG filter region —
feMorphologydilate expands beyond the default filter area, clipping the outline
When to Use
-webkit-text-stroke + paint-order
Most CSS-only text outline cases. Short, readable, and easy to maintain.
text-shadow
Thin fallback outlines only. Stop at 1px if quality matters.
SVG <text> with stroke
When outline quality is part of the design. Best browser-native option for thick Japanese display text.
SVG feMorphology
When the outline must stay fully outside the glyph. That requirement is uncommon, but no other CSS/SVG technique does it.
Beyond CSS
For image composition and export-quality rendering, Canvas 2D <code>strokeText()</code> / <code>fillText()</code>, opentype.js text-to-path conversion, and canvas libraries like Fabric.js or Konva.js provide better programmatic control.