CSS で縁取り文字を表現するのに、まず考えられるのは -webkit-text-stroke
を使用する方法で、以下のようなコードになります。
.text {
-webkit-text-stroke: 1px #000;
}
ここで、color: transparent
を指定すると塗り色が透明になり、縁取りの罫線のみでテキストを表示することができます。
.text {
color: transparent;
-webkit-text-stroke: 1px #000;
}
以下は、フォントファミリーに「Verdana」を指定した例です。
-webkit-text-stroke とバリアブルフォントの問題
見出し「-webkit-text-stroke とバリアブルフォントの問題」この -webkit-text-stroke
ですが、さきほどの塗りを透明にした状態(color: transparent
)でバリアブルフォント(Variable Fonts)を指定すると、文字の内側まで線が引かれてしまう問題があります。
例えば、Google フォントで人気の高い「Montserrat」や「Noto Sans Japanese」はバリアブルフォントですが、-webkit-text-stroke
を使用すると以下のような見え方になります。
以下は該当する GitHub Issue です。
- Montserrat and -webkit-text-stroke overlapping | Issue #4212 | google/fonts(外部リンクを開く)
- Issue when using -webkit-text-stroke | Issue #292 | rsms/inter(外部リンクを開く)
GitHub Issue でコメントされていますが、この問題の原因は Google フォント側ではなく、ウェブブラウザのバグもしくは制約にあるようです。
またこの問題は、ウェブブラウザにとどまらずデザインツールも対象です。ただし、ツールによって差があり、Figma では問題なく表示されますが、Adobe XD や Affinity Designer では同様の現象が発生するようです。
裏を返せば、Adobe XD や Affinity Designer であればデザイン時にこの問題に気付きますが、Figma を使った場合には、コーディングの段階まで気づかない可能性があるといえます。
この問題を解消する代替案として、以下が考えられます。
テキストを画像化
見出し「テキストを画像化」テキストを画像化する方法では確実に問題を解消できますが、メンテナンス性やパフォーマンス(ファイルサイズ)、アクセシビリティ、テキスト操作(コピーなど)ができなくなることとのトレードオフになります。
CSS の box-shadow
プロパティを使用
見出し「CSS の 」box-shadow
でも縁取り文字を実現することができますが、テキストを透明にすることができないため、利用ケースは限定されます。
また、塗りがある状態でも、box-shadow
を使用してなめらかな縁取りを表現するにはコードが複雑になるので、メンテナンス性を犠牲にすることになります。加えて、文字の形やフォントサイズ、罫線のサイズによっては、きれいな縁取りがされないことがあります。
SVG フィルタを使用
見出し「SVG フィルタを使用」SVG フィルタを使用して、縁取り文字を実現する方法です。CSS の filter
プロパティを使用することで、HTML 要素にも SVG フィルタを適用できます。
本記事では、この SVG フィルタを使用する方法で考えていきます。
feMorphology と feComposite
見出し「feMorphology と feComposite」SVG には feMorphology
というフィルタがあります。このフィルタを使用すると、対象となるソース(テキストなど)を太くしたり細くする効果が得られます。
feMorphology
では、以下の属性を指定できます。
属性 | 内容 |
---|---|
in | 入力名。指定がない場合には SourceGraphic (元のグラフィック要素)または、直前のフィルタの出力結果が対象となる |
operator | フィルタ効果のタイプ。dilate (膨張)または erode (侵食) |
radius | 半径の値。値が 1 つの場合には XY 軸をまとめて指定。値が 2 つの場合には X 軸と Y 軸それぞれを指定 |
result | 出力名。指定がない場合にはフィルタの最終結果になるか、次のフィルタの入力値になる |
operator
属性で、dilate
か erode
のいずれかのフィルタの効果を指定できます。文字を縁取るケースではどちらも使用できますが、見た目の最終結果が異なります。
上図ではデザインツール上で太さを調整していますが、実際にフィルタを適用したときには、輪郭の精細さがもう少し低くなります。
以下は、HTML のテキスト要素に対して、dilate
と erode
をそれぞれ指定した例です。
svg
要素内で filter
要素を指定して id
属性を付与しておき、CSS の filter: url(#id)
でフィルタを適用しています。
結果は以下のようになります。
このように、feMorphology
を使用することで、文字を太くしたり(dilate
)、細くしたり(erode
)できますが、この状態に feComposite
を組み合わせて文字の縁取りを実現します。
feComposite
では、2 つの要素を合成するフィルタです。指定できる属性は以下のとおりです。
属性 | 内容 |
---|---|
in | 入力名。指定がない場合には SourceGraphic (元画像)または、直前のフィルタの出力結果が対象となる |
in2 | in と合成する入力名 |
operator | 合成方法。Porter-Duff Compositing をベースとした 6 つのキーワードもしくは arithmetic を指定 |
result | 出力名。指定がない場合にはフィルタの最終結果になるか、次のフィルタの入力値になる |
operator
で使用できる 6 つのキーワードは over
、in
、out
、atop
、xor
、lighter
で、デフォルト値は over
です。
この記事のサンプルでは、out
と xor
をそれぞれ使用します。
dilate を使用した方法
見出し「dilate を使用した方法」SVG フィルタで縁取り文字を実現するには、feMorphology
に operator="dilate"
を指定する方法と、operator="erode"
を指定する方法が考えられます。
まずは operator="dilate"
を指定する方法を見ていきます。
<p class="dilate">
Lorem ipsum
</p>
<svg aria-hidden="true">
<filter id="dilate">
<feMorphology operator="dilate" radius="1" result="DILATED"></feMorphology>
<feComposite in="DILATED" in2="SourceGraphic" operator="out"></feComposite>
</filter>
</svg>
<style>
p {
font-family: Verdana, sans-serif;
font-size: 4rem;
font-weight: bold;
}
.dilate {
filter: url(#dilate);
}
</style>
dilate
を指定することで、radius
の分だけ外側に膨張します。
加えて feComposite
で直前のフィルタの結果を in
、元のテキストを in2
とし、operator="out"
を指定することで、重なった元のテキスト部分が取り除かれます。
その結果、以下のように縁取り文字を実現することができます。
Lorem ipsum
erode を使用した方法
見出し「erode を使用した方法」次に、feMorphology
に operator="erode"
を指定する方法を見ていきます。
<p class="erode">
Lorem ipsum
</p>
<svg aria-hidden="true">
<filter id="erode">
<feMorphology operator="erode" radius="1" result="ERODED"></feMorphology>
<feComposite in="ERODED" in2="SourceGraphic" operator="xor"></feComposite>
</filter>
</svg>
<style>
p {
font-family: Verdana, sans-serif;
font-size: 4rem;
font-weight: bold;
}
.erode {
filter: url(#erode);
}
</style>
erode
を指定することで、radius
の分だけ内側に侵食します。
さきほどの応用ですが、feComposite
で直前のフィルタの結果を in
、元のテキストを in2
とし、operator="xor"
を指定することで、2 つのソースが重ならない部分が結合されます。
こちらでも、縁取り文字を実現することができます。
Lorem ipsum
スタイルの比較
見出し「スタイルの比較」dilate
と erode
の結果を比較すると、フィルタ効果の違いにより、文字の太さが異なりますが、-webkit-text-stroke
で実装した場合と見え方が近いのは erode
です。
Lorem ipsum
Lorem ipsum
Lorem ipsum
しかし、次項で説明しますが、Safari の描画上のバグを踏まえると、erode
ではなく dilate
を採用するのが無難なように思えます。
SVG フィルタの欠点
見出し「SVG フィルタの欠点」そもそも、-webkit-text-stroke
の問題を解消するために SVG フィルタを取り入れましたが、この SVG フィルタを使用した方法にもいくつか欠点があります。
Safari のバグ
見出し「Safari のバグ」上記の SVG フィルタを使用した方法では、Safari にて描画上のバグがあるようです。
1. 斜体(italic
)のバグ
見出し「1. 斜体(」font-style: italic
のように斜体のスタイルを指定すると、最後の文字の右上が切り取られることがあります。
dilate
と erode
の指定では最後の「局」の右上部分が切り取られている2. erode
のバグ
見出し「2. 」こちらは発生する条件がわからないのですが、erode
でフィルタをかけたときに謎の線が引かれることがあります。dilate
を使用したときにはこの現象は見受けられませんでした。
erode
の指定で文字の内側に線が入ることがあるそのほかにも、iOS、macOS ともに、Safari での SVG フィルタの描画は若干不安定なようで、dilate
、erode
ともに文字の一部が切り取られるような現象が起きることがありました。
ホバー効果
見出し「ホバー効果」以下のように、ホバー時に縁取りから文字が塗られた状態にアニメーション(transition
)をともなう変化をつけたい場合には工夫が必要になります。
まず、アニメーション(transition
)をつけない場合には、以下のコードで実現できます。
<p>
<a href="#" class="dilate">
Lorem ipsum
</a>
</p>
<svg aria-hidden="true">
<filter id="dilate">
<feMorphology operator="dilate" radius="1" result="DILATED"></feMorphology>
<feComposite in="DILATED" in2="SourceGraphic" operator="out"></feComposite>
</filter>
</svg>
<style>
p {
font-family: Verdana, sans-serif;
font-size: 4rem;
font-weight: bold;
}
.dilate {
filter: url(#dilate);
}
.dilate:hover {
filter: none;
}
</style>
このように、単純にホバー時に filter: none
を指定するだけです。
対して、アニメーション(transition
)をつける場合には若干複雑になります。
<p>
<a href="#" class="dilate" aria-label="Lorem ipsum">
Lorem ipsum
</a>
</p>
<svg aria-hidden="true">
<filter id="dilate">
<feMorphology operator="dilate" radius="1" result="DILATED"></feMorphology>
<feComposite in="DILATED" in2="SourceGraphic" operator="out"></feComposite>
</filter>
</svg>
<style>
p {
font-family: Verdana, sans-serif;
font-size: 4rem;
font-weight: bold;
}
.dilate {
display: inline-block;
position: relative;
color: transparent;
}
.dilate::before,
.dilate::after {
content: attr(aria-label);
position: absolute;
top: 0;
left: 0;
color: #000;
}
.dilate::before {
filter: url(#dilate);
}
.dilate::after {
opacity: 0;
transition: opacity 0.4s ease;
}
.dilate:hover::after {
opacity: 1;
}
</style>
元のテキストは color: transparent
で透明にしておきます。
::before
擬似要素、::after
擬似要素に、aria-label
で指定したテキストを表示して、position: absolute
で元のテキストの上に重ねます。
::before
には、filter: url(#dilate)
で縁取り文字のスタイルを指定しており、::after
は初期状態で透明(opacity: 0
)にしておき、ホバー時にアニメーションをともなって表示するようにしています。
ちなみに aria-label
属性にしているのは、Google 翻訳などの機械翻訳を使用したときに翻訳の対象にするためです。しかし、場合によっては aria-label
が翻訳の対象にならないこともあるので 1、この点においても課題を残します。
そのため、このように擬似要素を使用するのではなく、2 つの要素(<span>
など)に同じテキストを指定し、一方に aria-hidden="true"
を指定したほうが確実かもしれません。
id 属性の管理
見出し「id 属性の管理」ページ内で id
属性の値はユニークである必要があるので、<filter>
に指定する id
が重複しないように注意が必要です。
SVG 要素の最適化
見出し「SVG 要素の最適化」フィルタのためだけに用意した SVG 要素がサイズを持ってしまい、周囲のレイアウトに影響を及ぼすので、CSS でサイズを無効にする必要があります。
また、スクリーンリーダーに認識させないために aria-hidden="true"
を指定します。
<p class="dilate">
Lorem ipsum
</p>
<svg aria-hidden="true">
<filter id="dilate">
<feMorphology operator="dilate" radius="1" result="DILATED"></feMorphology>
<feComposite in="DILATED" in2="SourceGraphic" operator="out"></feComposite>
</filter>
</svg>
<style>
svg {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.dilate {
filter: url(#dilate);
}
</style>
なお、SVG 要素に対して display: none
を指定する方法も考えられますが、Firefox ではフィルタが無効化されてしまいます。
SVG フィルタを外部ファイル化する
見出し「SVG フィルタを外部ファイル化する」SVG 自体を外部参照するように、SVG を外部ファイル化して、CSS の filter
プロパティで参照することができます。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="dilate">
<feMorphology operator="dilate" radius="1" result="DILATED"></feMorphology>
</filter>
</svg>
.dilate {
filter: url('./filter.svg#dilate');
}
この方法であれば SVG 要素のサイズの問題も id
属性の重複も気にしなくてよく、外部ファイルで一元管理できるためメンテナンス性の観点からも望ましいのですが、残念ながら Safari が対応していないようです。
ただし、使用できる場合でもファイルリクエストが発生するため、パフォーマンス面への影響は考慮する必要があります。
現時点で把握している SVG フィルタの懸念点は以上ですが、さらに詳しく見ていくとそのほかのバグが見つかるかもしれません。
おわりに
見出し「おわりに」今回、-webkit-text-stroke
とバリアブルフォントの問題の解決を出発点に SVG フィルタでの縁取り文字の実装方法を説明しました。後半で見てきたように、SVG フィルタにも懸念すべき点がいくつかあり、ベストな選択肢とは言い難いかもしれません。
そもそもブラウザのバグが解消されれば悩まなくてすむのですが、現時点では縁取り文字の表現にはリスクがあることを認識しておいた方がよいでしょう。
もし、今回紹介したよりも適切な方法が見つかりましたら、改めて記事にできればと考えています。
参考文献
見出し「参考文献」本記事の作成にあたり、以下のウェブページを参考にしました。
- SVG Filter Effects: Outline Text with <feMorphology> | Codrops(外部リンクを開く)
- <feComposite> | MDN(外部リンクを開く)