zudo-css-wisdom

Type to search...

to open search from anywhere

Text Outline and Stroke Effects

CreatedApr 3, 2026UpdatedApr 24, 2026Takeshi Takatsudo

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-stroke with paint-order: stroke fill for standard UI text and moderate outlines.
  • Use text-shadow only as a fallback for very thin (1px) outlines.
  • Use SVG <text> with stroke for the cleanest browser-native outline quality at any thickness.
  • Use SVG feMorphology only when you need a true outside-only outline.

Code Examples

-webkit-text-stroke + paint-order

-webkit-text-stroke without and with 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-direction, 8-direction, and dense text-shadow outlines
/* 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 text with stroke, stroke-width, stroke-linejoin, and 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

SVG feMorphology outside-only outline
<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

Side-by-side comparison of all techniques

Quick Reference

TechniqueThick Outlines (3-5px+)Edge QualityCJK CompatibilityUse Case
-webkit-text-stroke + paint-orderGoodGoodGood with paint-orderGeneral UI text and headings
text-shadow outline hackPoorLowFair for thin outlines onlyThin fallback outlines
SVG <text> with strokeExcellentExcellentExcellentDisplay text and precise rendering
SVG feMorphology filterExcellentExcellentExcellentTrue outside-only outline

Common AI Mistakes

  • Using -webkit-text-stroke without paint-order — the stroke eats glyph interiors, especially on CJK text where dense counters close up
  • Using only 4 text-shadow values — the result is a diamond shape, not a circular outline. Eight directions are the minimum for 1px
  • Applying thick text-shadow outlines — 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 default miter join
  • Not enlarging the SVG filter regionfeMorphology dilate 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.

References

Revision History