「ロゴが黒い四角になっているんですけど」
クライアントからそう連絡が来たのは、リリースの数日後でした。手元のChromeで開くと、何の問題もなく表示される。慌ててiPhoneで確認して、ようやく血の気が引きました。Safariでだけ、ロゴが真っ黒な長方形に化けている。
この記事は、そこから原因にたどり着くまでの記録です。結論を先に言うと、犯人はIllustratorの書き出し設定でした。よく紹介されている「<base> タグを消す」という対処法は、今回まったく効きません。というより、そのバグはすでに修正済みです。
こんな人に読んでほしい
- SVGがSafariやiPhoneでだけ黒くなって困っている
- 検索して出てきた対処法を試したけれど、直らなかった
- Illustratorから書き出したSVGを、サイトに埋め込んで使っている
特定のフレームワークやCMSに依存する話ではありません。Illustratorから書き出したSVGをページに埋め込んでいれば、どんな環境でも起こりえます。
急いでいる人へ
まず fill="url(#a)" を fill="url(#a) red" に書き換えてみてください。赤く表示されたら、参照の解決に失敗していることが確定します。あとはこの記事の後半を読めば原因が絞れます。
症状:Safariだけが黒くなる
改めて整理すると、こうでした。
| 環境 | 表示 |
|---|---|
| Chrome / Firefox / Android | 正常 |
| iOS Safari / macOS Safari | ロゴの位置に黒い矩形 |
特定のブラウザだけで壊れると、つい「Safariの実装が悪い」と考えたくなります。私もそう思いました。でも結果的には、マークアップ側に問題があり、それがSafariでたまたま露見しただけでした。
最初に疑って、外れた仮説
「SVG Safari 黒くなる」で検索すると、ほぼ確実に出てくる答えがあります。
HTMLに
<base>タグがあると、url(#id)がbase URLからの相対として解決されてしまい、参照先が見つからず黒くなる
理屈は通っています。実際、Angularで同じ問題を踏んだ事例では、<base href="/"> を消して解決したと報告されています。
でも、今回のサイトに <base> タグはありませんでした。
ここで手が止まったので、そもそもこの説がまだ生きているのか確認することにしました。WebKitのバグ票を追いかけたところ、こう書かれていました。
ステータスは RESOLVED FIXED。2019年9月2日にコミット(r249416)されてクローズしています。関連するBug 196950・200028も、重複としてまとめられていました。
仕様側も決着済みです。CSS Valuesが、# で始まる url() にはlocal urlフラグを立て、base URLに関わらず常に現在の文書に対して解決すると定義しています。
つまり 「<base> タグを消せ」は、6年以上前に修正されたバグへの対処法でした。今でも検索上位に出てきますが、現行のSafariで疑うべき相手ではありません。
ちなみに「WebKitは昔からこの手の解決が弱い」という通説もよく見かけますが、これも怪しい。2011年のW3Cバグ票を見ると、当時はChromiumとFirefoxが黒く描画していて、Safari 5.1.2は正常でした。Blinkが先に直し、WebKitが追随した、というのが実際の順序です。
真犯人は、たった1行の矩形だった
仮説が外れたので、SVGのコードを地道に読むことにしました。そして見つけたのがこれです。
<rect width="150" height="24" fill="url(#a)"></rect>
一見なんの変哲もない1行ですが、2つの違和感があります。
ひとつは id="a" という、あまりに短い参照名。もうひとつは、ロゴとほぼ同じ寸法なのに中身が何もない矩形が存在していること。
この2つを追いかけたら、両方ともIllustratorに行き着きました。
違和感①:a というIDはどこから来たのか
まず、よく見かける説明を訂正しておきます。
「Illustratorは自動で a、b、c と短いIDを振る」——これは正確ではありません。
Illustratorの書き出しダイアログには Object IDs というドロップダウンがあり、Layer Names / Minimal / Unique の3つから選べます。推奨は Layer Names で、選ぶと各要素にレイヤー名がそのままIDとして付きます。Minimal は短い文字と数字、Unique は長いランダム文字列になる、という違いです。
つまり id="a" は、Minimal が選ばれていた証拠。自動でそうなるのではなく、誰かがそう設定したということです。
……と思ったら、設定を無視するバグがあった
デザイナーの設定ミスかと思いきや、話はもう少し複雑でした。Adobeのフィードバックフォーラムに、こんな記録が残っています。
「Minify」を選ぶと「Object IDs」の選択が尊重されない不具合が、一部のIllustratorバージョンに存在した(現在は修正済み)
当時のコメント欄が生々しいので、いくつか引きます。「レイヤー名がパスIDになっていたのに、新しい a、b というIDが確立した仕組みを壊した。修正されるまでIllustrator 25を使わざるを得ない」「26.0.3にダウングレードして解決した」——Illustrator 26.x〜27.0.1 のころの話です。
Layer Names を正しく選んでいても、Minify にチェックを入れていれば a が出力される。 そんな時期が実在したわけです。納品物を責める前に、書き出しに使われたIllustratorのバージョンを確認する価値があります。
違和感②:なぜ「不要な矩形」が混ざるのか
もうひとつの謎、中身のない矩形。これもIllustratorの仕様で説明がつきます。
SVG書き出しは、PNGなどのラスター書き出しと違って、非表示レイヤーやマスクの裏にあるオブジェクトまで含めます。
Adobeコミュニティの回答が的確でした。
クリッピングマスクの裏に隠れたオブジェクトも計算に入るという報告もあります。
極めつけは、Adobeのコミュニティマネージャー自身が「隠れたマスクが図形を切っていないか確認し、アートボード外に不要な透明の矩形やパスがないか調べて削除する」と助言していること。
つまり 「不要な透明の矩形」は、Adobeが公式に想定しているほど定番の残骸なんですね。デザイナーの画面では見えていないので、消し忘れが起きるのも無理はありません。
なぜ「黒」だったのか
ここまでで、参照が壊れていることは分かりました。でも疑問が残ります。なぜ他の色ではなく、よりによって黒なのか。
答えは拍子抜けするほど単純で、fill の初期値が black だからです。
仕様は url() のあとにフォールバック色を書くことを認めています。
fill: url("#a") magenta; /* 解決できなければ magenta */
これを書かず、参照も解決できない。だから初期値がそのまま出る。それだけの話でした。
なお「Safariが仕様どおり黒を出している」と説明されることがありますが、これも正確ではありません。仕様が定めているのは「フォールバックがあれば使う」までで、無い場合の扱いはSVG 1.1が「文書エラー」、SVG Tiny 1.2が「none 扱い」と、版によって揺れています。黒いのはあくまで初期値の話です。
逆に言えば、フォールバックを1語書いておくだけで、最悪の見た目は避けられたということでもあります。
なぜSafariでだけ壊れたのか
最後の謎が残っています。参照が壊れているなら、Chromeでも黒くなるはずでは?
鍵は ID衝突でした。
Minimal で書き出すと、すべてのSVGが a から始まるIDを持ちます。 ロゴもアイコンも図版も、それぞれ独立して書き出せば、全ファイルが a、b、c を名乗る。これを同じページにインラインで展開すれば、衝突しないほうが不思議です。
そして衝突したとき、どの定義が勝つか、どのタイミングで参照が切れるかは、レンダリング順やDOM操作のタイミングに左右されます。「Safariだけ」に見えたのは、その微妙な差でWebKitだけが露見しただけでした。
これはAdobeも認識していて、ちゃんと答えを用意しています。
つまり Minimal は「1ファイルを単体で使う前提」の設定。インライン展開するなら選んではいけない選択肢だったわけです。
書き出し後にSVGOを通している場合、リスクはもう一段増えます。IDの短縮を有効にすると参照だけ残して linearGradient が削除される報告や、複数SVGをインラインするならプレフィックスが必要という指摘があります。
結局どう直したか
今回はその <rect> を削除して終わりました。 表示に必要のない要素だったので、これが最短でした。
同じ症状に当たった人のために、切り分けの順番を残しておきます。
-
fill="url(#a) red"に書き換える → 赤くなれば参照解決の失敗が確定 -
document.getElementById('a')→nullなら参照先が存在しない -
document.querySelectorAll('[id="a"]').length→ 2以上ならID衝突 - 混入元をgrepで特定する
grep -rn 'url(#a)' . --include=*.svg --include=*.html --include=*.jsx
CMSを使っている場合は、編集者が記事本文に直接貼り付けていることもあります。テンプレートを探しても見つからないときは、コンテンツ側のデータベースも確認してみてください。
二度と踏まないために
デザイナーと共有したいこと
- Object IDs は Layer Names。複数のSVGをページに埋め込むなら Unique。Minimal は単体利用が前提
- Minify と併用したとき、IDが
aになっていないか書き出し後のコードを目視する - 書き出し前に、非表示レイヤー・アートボード外・マスク裏のオブジェクトを削除する(
Object > Clipping Mask > Releaseで確認) - テキストはアウトライン化する
実装側でやること
-
fill="url(#grad) #4a90d9"のようにフォールバックを必ず書く。1語で最悪の事態を防げます - インライン展開するなら、ビルド時にIDへプレフィックスを付ける
おわりに
今回いちばん時間を無駄にしたのは、すでに修正されたバグへの対処法を試していた時間でした。
<base> タグの話は、当時は間違いなく正しい情報でした。ただ、それが修正されたあとも記事だけが残り、検索上位に居座り続けている。同じことは、Illustratorの「自動でaが振られる」という説明にも言えます。
「みんなが言っているから正しい」は、技術の世界では特に賞味期限があるようです。バグ票のステータスを見に行くだけで、無駄な回避策をひとつ捨てられる。 そのことを、黒い長方形に教わりました。