問題の概要
画像にSVGの clipPath を使って複雑な形でマスク(切り抜き)する実装において、ChromeやFirefoxでは正常に表示されるものの、Safariだけクリッピングが効かず画像が四角いまま表示される現象が発生しました。
原因となっていたのは、CSSから外部SVGファイル内の clipPath を参照しているケースです。
.masked-element {
clip-path: url("/assets/images/shapes.svg#my-clip-path");
}
原因
Safari(WebKit系)では、CSSの clip-path: url("external.svg#id") のように、外部SVGファイル内の clipPath を参照する挙動が不安定です。
同一オリジンのファイルであっても適用されなかったり、HTML内の <base> タグの影響でURL解決が崩れたりします。ChromeやFirefoxでは問題なく表示されていても、Safariだけマスクが外れることがあります。
一方で、同じHTML内に定義した clipPath を url(#id) で参照する方法は比較的安定して動作します。今回は外部SVG参照をやめ、インラインSVGに切り替えることで解決しました。
解決方法
外部SVGファイルを参照せず、HTML内に defs > clipPath を直接埋め込みます。CSS側では、ファイルパスを含めず url(#id) だけで参照します。
1. HTMLにインラインSVGを追加する
対象ページ、または共通レイアウトファイルの body 直下やコンテンツの先頭付近に、不可視のSVGとして clipPath 定義を置きます。
<div class="layout-wrapper">
<svg
aria-hidden="true"
focusable="false"
width="0"
height="0"
style="position:absolute; left:-9999px; overflow:hidden;"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath id="mask-shape-01" clipPathUnits="objectBoundingBox">
<path d="M0.595,0.014 C0.634,-0.01 0.687,-0.003 ... Z" />
</clipPath>
<clipPath id="mask-shape-02" clipPathUnits="objectBoundingBox">
<path d="M0.903,0 C0.956,0 1,0.034 1,0.076 ... Z" />
</clipPath>
</defs>
</svg>
<div class="masked-image">
<img src="photo.jpg" alt="">
</div>
</div>
このSVGは定義用であり、画面には表示しません。width="0"、height="0" に加えて画面外へ退避させておくと、レイアウトへの影響を避けられます。
表示目的のSVGではないため、スクリーンリーダーやキーボード操作の対象にならないよう aria-hidden="true" と focusable="false" も付与します。また、ブラウザ間でSVGとして解釈される前提を揃えるため、xmlns も明記します。
2. CSS側を url(#id) に変更する
CSSでは外部ファイルのパスを書かず、HTML内に定義したIDだけを参照します。
.masked-image {
-webkit-clip-path: url(#mask-shape-01);
clip-path: url(#mask-shape-01);
}
古いSafariや一部環境で挙動が変わる場合に備え、-webkit-clip-path も併記しておきます。
実装時の注意点
IDの重複に注意する
インラインSVGはHTML内に直接配置するため、id="mask-shape-01" はページ内で一意にする必要があります。
共通ヘッダーや共通フッターに埋め込む場合は、他のIDと重複しないようプロジェクト固有の接頭辞を付けると安全です。
<!-- 避けたい例 -->
<clipPath id="mask">
<!-- 安全な例 -->
<clipPath id="site-hero-mask-01">
mask や clip のような短いIDは、他のSVGやライブラリと重複する可能性があります。
objectBoundingBox を使う場合は座標を0〜1にする
clipPathUnits="objectBoundingBox" を指定する場合、path の座標は要素サイズに対する比率(0〜1 の範囲)で記述する必要があります。
元のSVGが width="300"、height="400" のような絶対座標で作られている場合、そのままコピーしてもマスク位置は合いません。以下の計算式で相対座標へ変換します。
相対X座標 = 元のX座標 / SVGの幅
相対Y座標 = 元のY座標 / SVGの高さ
たとえば幅300pxのSVGならX座標に 1 / 300 を掛け、高さ400pxならY座標に 1 / 400 を掛けて変換します。
相対座標化が難しい場合は、clipPathUnits="userSpaceOnUse" を使い、SVG側の viewBox で制御する方法もあります。ただし、CSS側の要素サイズが変わったときにマスクが追従しにくくなります。レスポンシブ対応を行う場合は、objectBoundingBox に合わせて座標を変換するほうが扱いやすくなります。
なぜこの方法にしたか
今回の目的は、Safariでも確実にマスクを効かせることです。
外部SVG参照のままでは、ブラウザ差分、パス解決、キャッシュ、<base> タグなど、原因の切り分けに手間がかかります。Chromeでは表示されているのにSafariだけ崩れる事象も起こりやすくなります。
インラインSVGにしておけば、CSSから同一ドキュメント内のIDを参照するだけで済みます。外部ファイルの読み込みやURL解決に依存しないため、表示が安定します。
パフォーマンス面では外部SVGへのリクエストが1つ減る程度ですが、表示の安定性と保守性の面でメリットがあります。
まとめ
Safariで clip-path: url("external.svg#id") が効かない場合は、外部SVG参照をやめ、HTML内にインラインSVGの defs > clipPath を配置する方法が有効です。
CSS側は url(#id) で参照します。
.masked-image {
-webkit-clip-path: url(#mask-shape-01);
clip-path: url(#mask-shape-01);
}
ブラウザごとの差分に左右されないよう、特にSafari対応が必要な案件では同一ドキュメント内で完結させる構成が無難です。