## 問題の概要
画像にSVGの clipPath を使って、複雑な形でマスク(切り抜き)する実装で、ChromeやFirefoxでは正常に表示されるのに、Safariだけクリッピングが効かず、画像が四角いまま表示されることがありました。
問題になったのは、CSSから外部SVGファイル内の clipPath を参照しているケースです。
```css
.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" に加えて、画面外へ退避させておくとレイアウトへの影響を避けられます。
aria-hidden="true" と focusable="false" も付けておきます。表示目的のSVGではないため、スクリーンリーダーやキーボード操作の対象にしないためです。
xmlns も明記しておいた方が無難です。ブラウザ間でSVGとして解釈される前提を揃えられます。
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 も併記しておきます。古いSafariや一部環境で効き方が変わることがあるためです。
実装時の注意点
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 の座標は要素サイズに対する比率で書く必要があります。つまり、X座標もY座標も基本的に 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対応が必要な案件では、この形にしておくのが無難です。