テキストのアウトラインとストローク効果
問題
テキストのアウトライン効果は一見シンプルに見えますが、レンダリングの詳細はシンプルではありません。
中央揃えのストロークはグリフの内側に食い込みます。これはラテン文字のカウンター(O、P、R など)で目立ちます。縁 や 取 のような画数の多い CJK 文字ではさらに顕著です。text-shadow で作る太いフェイクアウトラインも品質がすぐに劣化します。角がカクカクになり、曲線が膨らんでしまいます。
アウトラインテキストに求められる要件:
- CSS ベースの実装
- 暗い背景でも読みやすいこと
- 日本語と英語の両方で安定していること
- ブラウザサポートとアウトラインの太さのトレードオフが明確であること
解決方法
アウトラインの太さとレンダリング要件に応じた手法を使い分けます。
- 標準的な UI テキストや中程度のアウトラインには、
-webkit-text-strokeとpaint-order: stroke fillを使います。 text-shadowは非常に細い(1px)アウトラインのフォールバックとしてのみ使います。- ブラウザネイティブで最もクリーンなアウトライン品質を得るには、SVG
<text>とstrokeを使います。 - 真の外側のみのアウトラインが必要な場合にのみ、SVG
feMorphologyを使います。
コード例
-webkit-text-stroke + paint-order
/* paint-order なし — ストロークがグリフ内部を侵食する */
.text-outline {
color: hsl(48 100% 68%);
-webkit-text-stroke: 6px hsl(336 80% 58%);
}
/* paint-order あり — フィルがストロークの内側半分を覆う */
.text-outline-improved {
color: hsl(48 100% 68%);
-webkit-text-stroke: 6px hsl(336 80% 58%);
paint-order: stroke fill;
}
-webkit-text-stroke は中央揃えのストロークを描画します。半分が内側に、半分が外側に広がります。内側の半分がカウンターや内部の隙間を狭めるため、太いウェイトや CJK テキストで特に目立ちます。
paint-order: stroke fill はストロークを先に描画し、その上にフィルを重ねます。フィルが内側半分を覆い隠すため、ジオメトリは中央揃えのままでも、見た目は外側ストロークに近くなります。
CJK に関する注意: 日本語のグリフは内部構造が密です。太い中央揃えストロークはラテン文字よりも早く細部の隙間を塞いでしまいます。CJK テキストで -webkit-text-stroke を使う場合は、必ず paint-order: stroke fill を追加してください。
ブラウザサポート: -webkit-text-stroke は 2017 年 4 月から Baseline です。CSS の paint-order は 2024 年 3 月に Baseline に到達しました。
text-shadow によるアウトラインハック
/* 4 方向 — ダイヤモンド形になり不十分 */
.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 方向 — 1px アウトラインの最低限の形 */
.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%);
}
太いアウトラインをプログラムで生成する方法:
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 は実際のストロークを描画するわけではありません。テキストのオフセットコピーを重ねているだけです。4 方向ではダイヤモンド形になります。8 方向が 1px のリングとして最低限使える形です。太いアウトラインには多数のシャドウエントリが必要になり、それでもカクカクした膨らんだ描画になります。
CJK に関する注意: 画数の多い漢字ほど弱点が早く現れます。ラテン文字よりも早く内部の細部がつぶれてしまいます。この手法は最大 1px に留めてください。
SVG テキストの 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 テキストはブラウザネイティブで最もクリーンなアウトライン品質を実現します。テキスト要素に対して stroke、stroke-width、stroke-linejoin、paint-order を直接制御できます。特に太いアウトラインでは、text-shadow よりもシャープなレンダリングが得られます。
太いアウトラインには stroke-linejoin="round" を設定してください。ラウンドジョインによって鋭い角のスパイクを防ぎ、密な曲線をより滑らかに保てます。
CJK に関する注意: 太いアウトラインとクリーンな角が必要な太い日本語ディスプレイテキストには、ブラウザネイティブの手法として最も優れた選択肢です。
SVG feMorphology フィルタ
<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>
このフィルタはソースアルファを膨張させ、拡大されたシルエットに色を付け、そこから元のアルファを除去し、元のテキストを上に合成します。結果として、純粋に外側のみのアウトラインが得られます。
この記事で紹介する手法の中で、真の外側のみのストロークを作れるのはこの手法だけです。ストロークの内側への侵食が許容できない場合に有用です。
フィルタ領域は x、y、width、height 属性で拡大する必要があります。デフォルトの領域のままだと、アウトラインがクリッピングされます。
CJK に関する注意: 内部のカウンターが完全に保持されます。複雑なレイアウトではフィルタのパフォーマンスとクリッピングをテストしてください。
比較
クイックリファレンス
| 手法 | 太いアウトライン(3-5px 以上) | エッジ品質 | CJK 互換性 | ユースケース |
|---|---|---|---|---|
-webkit-text-stroke + paint-order | 良好 | 良好 | paint-order 併用で良好 | 一般的な UI テキストや見出し |
text-shadow アウトラインハック | 不良 | 低い | 細いアウトラインのみ可 | 細いフォールバックアウトライン |
SVG <text> + stroke | 優秀 | 優秀 | 優秀 | ディスプレイテキストや精密なレンダリング |
SVG feMorphology フィルタ | 優秀 | 優秀 | 優秀 | 真の外側のみアウトライン |
AI がよくやるミス
-webkit-text-strokeをpaint-orderなしで使う — ストロークがグリフの内部を侵食します。特に CJK テキストでは密なカウンターが塞がってしまいますtext-shadowを 4 方向のみで使う — ダイヤモンド形になり、円形のアウトラインにはなりません。1px でも最低 8 方向が必要です- 太い
text-shadowアウトラインを適用する — 太いアウトラインには 20 以上のシャドウが必要で、それでもカクカクした膨らんだエッジになります - SVG テキストで
stroke-linejoin="round"を忘れる — 太いアウトラインの曲線や CJK の角で、デフォルトのmiterジョインによる鋭いスパイクが発生します - SVG フィルタ領域を拡大しない —
feMorphologyのdilateはデフォルトのフィルタ領域を超えて拡張するため、アウトラインがクリッピングされます
使い分け
-webkit-text-stroke + paint-order
ほとんどの CSS のみのテキストアウトラインに適しています。短く、読みやすく、保守も容易です。
text-shadow
細いフォールバックアウトラインにのみ使います。品質を重視するなら 1px で止めてください。
SVG <text> + stroke
アウトラインの品質がデザインの一部である場合に使います。太い日本語ディスプレイテキストに最適なブラウザネイティブの選択肢です。
SVG feMorphology
アウトラインがグリフの完全に外側に留まる必要がある場合に使います。その要件は一般的ではありませんが、他の CSS/SVG 手法では実現できません。
CSS の範囲を超えて
画像合成やエクスポート品質のレンダリングには、Canvas 2D の <code>strokeText()</code> / <code>fillText()</code>、opentype.js によるテキストからパスへの変換、Fabric.js や Konva.js などの canvas ライブラリがより優れたプログラム的制御を提供します。