Insights

技術情報

Safariで「外部SVG参照のclip-pathが効かない」問題の解決方法(インラインSVGで確実に直す)

## 問題の概要

画像に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内に定義した clipPathurl(#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">

maskclip のような短い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対応が必要な案件では、この形にしておくのが無難です。