使えば爽快! SVG Sprite
CSSスプライトやアイコンフォントはもう古い?
改訂版を出しましたので、そちらをご参照ください。
Safari8でSVG Fragment Identifiers(以下、svg#fragment)に対応してきた。 Firefox、Chrome、IEはすでに対応済みなので、これで主要ブラウザの足並みが揃った。 注:ところが、Safariの実装にはバグがあった…orz。 WebKit Bugzilla上では既に修正済みとなっているが、Safari9.xまでは依然としてバグが残っている。
これまで、PNGなどの画像を利用したCSSスプライトやアイコンフォントを使う気になれなかったが、svg#fragmentを使ったSVGスプライトならいけるな、と思い、折を見てテストを繰り返し、ようやくその方法が固まった。
そもそもCSSスプライトやアイコンフォントに対する個人的な不満は
- CSSスプライト
-
- 切り出す画像の領域を逐一CSSで指定しなければならない
- 繰り返し画像を敷き詰めることができない(できないこともないけど面倒らしい)
- Retinaとかに対応するにはあらかじめ画像を大きくしておくか、サイズ違いの画像を複数用意する必要がある
- アイコンフォント
-
- 色の変更は容易だが、単色しか使えない
- 「文字」に「画像」を割り当てているのでアクセシビリティ面で問題がある(Webフォントを無効化されている場合に顕著)
対して、SVGならばこれらの問題は全て解消される。 さらに、テキストデータなのでgzip圧縮かければかなり軽くもなる。
ただし、svg#fragmentに未対応のブラウザへのフォールバックを考慮すると用意する画像は結局多くなるのだが…orz
svg#fragmentを用いたSVGスプライトの利用方法
以下は、レガシー・ブラウザのこともある程度考慮しつつ、SVGスプライトを利用する方法の一提案。 まずは、svg#fragmentを利用したSVGスプライトのテストケース
SVGスプライト用ファイルのサンプルソース
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.1">
<title>SVG Sprite Sample</title>
<view id="rect" viewBox="150 0 128 128" />
<g transform="translate(150,0)">
<title>Rect</title>
<rect width="128" height="128" fill="rgb(255,0,0)" />
</g>
<view id="circle" viewBox="300 0 128 128" />
<g transform="translate(300,0)">
<title>Circle</title>
<circle cx="64" cy="64" r="64" fill="rgb(0,255,0)" />
</g>
<view id="triangle" viewBox="450 0 128 128" />
<g transform="translate(450,0)">
<title>Triangle</title>
<polygon points="0,128 128,128 64,0" fill="rgb(0,0,255)" />
</g>
</svg>
ポイントは
svg要素ではwidthとheightを指定不要、むしろつけるとAndroidブラウザでバグる- view要素のviewBox属性でクリップ領域を指定
この際、基準となるviewBox(0 0 width値 height値)の範囲からは外して配置 - g要素のtransform属性でviewBox属性の値に合わせて移動させる
- g要素の子孫要素で画像を指定
追加するスプライト画像の数は自由だが、アスペクト比(この例の場合は128:128=1:1)やviewBoxでのサイズ指定は同じものに限定しておくwidth/heightをつけないのでこの気遣いも無用- HTMLやCSSからは'sprite_sample.svg#circle'といったようにフラグメント識別子をつけて指定する
- なお、view要素のid値はフォールバック用個別画像のファイル名と一致させておくと吉(同じにしておけばメンテが楽w)
一方、フォールバック用の画像としては、rect.svg、circle.svg、triangle.svgの各ファイルを準備しておく。 (IE8などにも対応させる必要がある場合には、さらにそれぞれのPNG画像なども用意…)
とはいえ実際には個別画像の方を先に作っていることがほとんどだろうと思う。 その場合は個別画像をテキストエディタで開いて、中身(画像の指定部分)をスプライト用のSVGにコピペして、後はg要素でラップしてview要素を追加すればいいだけだったりするので、そんなに手間ではないはず。
CSSの背景画像としてSVGスプライトを使う場合
今のところ、Safari、Chrome、AndroidなどWebkit系のブラウザは、CSSのbackground-imageでのsvg#fragmentには対応していない。 従って、Webkit系にはフォールバックを用意する必要がある。 WebKit BugzillaBugzilla上ではFixされました、chromiumFixされました
/* .sampleクラスが付与された要素の背景に繰り返しスプライト画像を適用する例 */
/* 必要があればレガシー・ブラウザ向けに */
@media screen {
.sample {
background-image: url(path/to/sprite/circle.png);
background-repeat: repeat;
}
.sample:hover {
background-image: url(path/to/sprite/triangle.png);
}
}
/* SVGスプライト */
@media screen and (color) {
.sample {
background-image: url(path/to/sprite/sprite.svg#circle);
background-size: 3em auto;
}
.sample:hover {
background-image: url(path/to/sprite/sprite.svg#triangle);
}
}
/* Webkit系用の上書き指定。Retinaのことも考慮してSVGを使用 */
@media screen and (-webkit-min-device-pixel-ratio: 1) {
.sample {
background-image: url(path/to/sprite/circle.svg);
}
.sample:hover {
background-image: url(path/to/sprite/triangle.svg);
}
}
Chromeが2016年3月にリリース予定の49でCSSの背景画像でもSVGスプライトが利用できるようになったので、3月以降はこのように書いた方がおそらくすっきりする。
/* .sampleクラスが付与された要素の背景に繰り返しスプライト画像を適用する例 */
/* レガシー・ブラウザ */
@media screen {
.sample {
background-image: url(path/to/circle.png);
background-repeat: repeat;
}
.sample:hover {
background-image: url(path/to/triangle.png);
}
}
/* SVG対応ブラウザ (Safari、IE11以下、Androidブラウザ) */
@media screen and (color) {
.sample {
background-image: url(path/to/circle.svg);
background-size: 3em auto;
}
.sample:hover {
background-image: url(path/to/triangle.svg);
}
}
/* SVGスプライト対応ブラウザ (Firefox、Edge、Chrome) */
@media screen and (min-resolution: 1dppx) {
.sample {
background-image: url(path/to/sprite.svg#circle);
}
.sample:hover {
background-image: url(path/to/sprite.svg#triangle);
}
}
img要素でSVGスプライトを使う場合
HTML5にはimg要素にsrcset属性が追加されている。
このsrcset属性には、ChromeとSafari8はすでに対応済み。
とはいえ、Safari8は画素密度記述子(2xなど)への対応のみで幅記述子(100wなど)には対応していない。
また、Chromeも今のStable版は画素密度記述子にしか対応していないが、間もなく登場する38からは両方に対応している。
Firefoxも対応しているが現状ではabout:configからdom.image.srcset.enabledをtrueに設定する必要がある。
一方、IEは軒並み未対応で、来るIE12(?)についても、Under Consideration
ということで実装されるかどうかは今のところ不明という状況。
つまり、残念ながら、現時点ではsrcsetは無条件に使える状況には…ないorz
しかし、srcsetの値を拾えないわけでもない。
さらに、SVGをsrcset属性で使うには
<img src="sample.svg" srcset="sprite.svg#sample" />
とURLを追加するだけで良く、そもそもwもxも要らない(ベクター画像なので)。
そこで、svg#fragmentには対応しているもののsrcsetに未対応なブラウザ向けに、JavaScriptで画像を差し替える処理を加えることにした。
Safariへのバグ対応として、Safari用のフォールバックも付与。 Safari用フォールバック適用版のテストケース
JavaScript
/**
* svgSpriteFromSrcset
* svg#fragmentなスプライト画像をimg要素のsrcset属性から見つけて適用させるJavaScript
* svg#fragmentにバグがあるSafariには逆に適用させないようsrcsetを削除
* 注1:img要素にはsrcset属性が付与されていること
* 注2:srcset属性の値は、SVGスプライト画像のURLのみを指定すればOK(xやwは不要)
* (例)<img src="someId.svg" srcset="svgSprite.svg#someId" /> など
* Copyright (c) 2014-2016 Kazz
* http://asamuzak.jp
* Dual licensed under MIT or GPL
* http://asamuzak.jp/license
*/
(function(_win, _doc) {
"use strict";
function svgSpriteFromSrcset() {
function svgSpriteByDefault(o) {
return o.srcset && _win.matchMedia &&
_win.matchMedia("(min-resolution: 1dppx)").matches;
}
var x = _doc.querySelectorAll("img[srcset]"), y, z, a, i, l = x.length;
if((_doc.documentElement.matches || _doc.documentElement.msMatchesSelector) &&
l > 0 && !svgSpriteByDefault(x[0])) {
for(a = /([\w\/:%\$&\(\)~\.=\+\-]+(?:\.svgz?)?#[\-]?[a-zA-Z_][\w\-]+)/, i = 0; i < l; i++) {
y = x[i];
z = a.exec(y.getAttribute("srcset"));
z && (y.srcset ? y.removeAttribute("srcset") : y.setAttribute("src", z[1]));
}
}
}
_doc.addEventListener("DOMContentLoaded", svgSpriteFromSrcset, false);
})(window, document);
HTML
<ul class="sample">
<li><img src="./sprite/rect.svg" srcset="./sprite/sprite.svg#rect" alt="四角形" /></li>
<li><img src="./sprite/circle.svg" srcset="./sprite/sprite.svg#circle" alt="円形" /></li>
<li><img src="./sprite/triangle.svg" srcset="./sprite/sprite.svg#triangle" alt="三角形" /></li>
</ul>
ただし、この例では、SVGに対応していないIE8やAndroid2系の標準ブラウザなどで表示ができない。 SVG→PNGのフォールバックはいくつか方法があるのでググってお好みのものを探してもらうとして、mod_rewriteが使えれば手っ取り早く書き換えできる方法を1つ(先述のテストケースには未適用)。
.htaccess
<FilesMatch "\.svgz?$">
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} "(MSIE 8|Android 2)\."
RewriteRule ^(.+)\.svgz?$ $1.png
RewriteRule .* - [T=image/png,L]
</FilesMatch>
これによりレガシー・ブラウザはこれまで通りの画像が示される一方、svg#fragmentに対応しているブラウザはスプライト画像が適用されることになる。 srcsetを有効化していないFirefoxやIEの場合は、スプライト化によるGETリクエストの減少という恩恵は受けられなくなるが、暫定的な経過措置なのでスクリプトはいずれ外せばOK(いつになるかわからないけどw)
結論
まとめると、背景画像ではWebkit系で使えず、前景画像ではスクリプト無しにIEで使えない、つまり、背景でも前景でも使えるのはFirefoxだけ(それもsrcsetを有効化している場合に限るが)、というのがsvg#fragmentをとりまく今の状況。 でも、この先、状況が改善されることはあっても後退することはない…はず。
ということで、SVGスプライトははじける最先端の刺激、さらにキリっとクリアなベクターフレーバー (c) 日本コカ・コーラ
ぜひご利用を!
SVGスプライト化による最大の恩恵はGETリクエストの減少…のはずなのだが、Chromeは1回のGETリクエストであとは使いまわしているが、Firefox, Edge, IEはフラグメント識別子で参照している数だけGETリクエストを発行している…orz Bugzilla、 Microsoft Connect
心置きなく使えるようになるまでにはまだまだ先は長そう…
おまけ
SVG#fragmentのネタからは外れるが、上のスクリプトを応用してsrcset属性からSVG画像を見つけて適用させるスクリプトを書いてみた。
/*
* applySvgFromSrcset
* SVG画像をimg要素のsrcset属性から見つけて適用させるJavaScript
* 適用対象:Android3+, IE9+
* 注1:img要素にはsrcset属性が付与されていること
* 注2:srcset属性の値は、SVG画像のURLのみを指定すればOK(xやwは不要)
* (例)<img src="sample.png" srcset="sample.svg" /> など
* Copyright (c) 2015-2016 Kazz
* http://asamuzak.jp
* Dual licensed under MIT or GPL
* http://asamuzak.jp/license
*/
(function() {
"use strict";
(window.matchMedia || document.documentMode) && document.addEventListener("DOMContentLoaded", function() {
for(var x = document.querySelectorAll("img[srcset]"), y, z, a = /([\w\/:%\$&\(\)~\.=\+\-]+\.svgz?)/, i = 0, l = x.length; i < l && (y = x[i], !y.srcset); i++) {
(z = a.exec(y.getAttribute("srcset"))) && (y.src = z[1]);
}
}, false);
})();
"使えば爽快! SVG Sprite"へのTwitter上でのコメントやRT
45件のツイートがあります。
ツイート 1
asamuzaK.jp : 使えば爽快! SVG Sprite / CSSスプライトやアイコンフォントはもう古い? http://t.co/shbUUPFv8h
ツイート 2
フォールバックのJavaScriptを作り直して書き直した asamuzaK.jp : 使えば爽快! SVG Sprite / CSSスプライトやアイコンフォントはもう古い? http://t.co/shbUUPFv8h
ツイート 3
自サイトに適用した asamuzaK.jp : 使えば爽快! SVG Sprite / CSSスプライトやアイコンフォントはもう古い? http://t.co/shbUUPFv8h
ツイート 4
スクリプト修正 AndroidやSafari7の判別で意味不明なチョンボをしていた(でもなぜか判別できていたのだが…) asamuzaK.jp : 使えば爽快! SVG Sprite ~ CSSスプライトやアイコンフォントはもう古い? http://t.co/shbUUPFv8h
ツイート 5
スクリプトを微修正 asamuzaK.jp : 使えば爽快! SVG Sprite ~ CSSスプライトやアイコンフォントはもう古い? http://t.co/shbUUPFv8h
ツイート 6
img要素におけるIE8やAndroid2への対策案を追記 asamuzaK.jp : 使えば爽快! SVG Sprite ~ CSSスプライトやアイコンフォントはもう古い? http://t.co/shbUUPFv8h
ツイート 7
.@yoshiko_pg JavaScriptでSVGに差し替えてませんか? Webkitの当該バグは探してませんけど、 http://t.co/shbUUPFv8h のテスト中にSafari7でその現象に遭遇してました
ツイート 8
このあたりもYosemiteのSafariでテストしていただけると助かるんだけどなぁ…(ボソッ SVG sprite test http://t.co/QMlVRAl3KF http://t.co/shbUUPFv8h
ツイート 9
!(!window.CSS && !window.performance.now || window.CSS && !window.CSS.supports("(transform: none)")) をコンパイラにかけると…(続く) http://t.co/shbUUPFv8h
ツイート 10
(window.CSS || window.performance.now) && (!window.CSS || window.CSS.supports("(transform: none)")) 1文字短くなったがわかりにくいw http://t.co/shbUUPFv8h
ツイート 11
空白も削除されているから実際には7文字相当だけど… http://t.co/shbUUPFv8h
ツイート 12
そいえば、今月初めに次期IEのsrcsetのステータスがUnder Consideration -> In Developmentに変わった まだ先は長いだろうけど… asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/Cvr7dynKzs
ツイート 13
よく考えたら、 var q = '.sample > li > img[srcset]'; って要らないな 単にimg[srcset]を拾えばいいじゃん… 後で直そう asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/Cvr7dynKzs
ツイート 14
早速スクリプトを修正 http://t.co/Cvr7dynKzs
ツイート 15
SVG Fragment Identifiersとmatches()がほぼ一致しているのでsrcset適用の判別式を変更 (Blackberryは取りこぼすけど…) asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/Cvr7dynKzs
ツイート 16
SVG fragment identifiers http://t.co/T4y7lkerF5 matches() http://t.co/7aKZXv4oMC asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/Cvr7dynKzs
ツイート 17
あぁ、Blackberryだけじゃなくて31ベースのFirefox ESRも取りこぼしているな… でも31はsrcsetに未対応だったしまぁいいや…w asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/Cvr7dynKzs
ツイート 18
Safariのバグについて冒頭に注意書きを追記 asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/Cvr7dynKzs
ツイート 19
asamuzaK.jp : 使えば爽快! SVG Sprite ~ CSSスプライトやアイコンフォントはもう古い? http://t.co/W0qemy4XuG > 背景画像ではWebkit系で使えず
ツイート 20
つーことは、近々これが再び使えるようになる! http://t.co/KxOnXf9XCl https://t.co/iLp12DobQe
ツイート 21
MS edgeでテスト インラインSVGの固有サイズ表示はSVG1.1仕様になっている♪ http://t.co/cZqExeogsj SVG#fragmentでimg srcsetが使える♪ http://t.co/KxOnXf9XCl
ツイート 22
.@rikuo onerrorのフォールバックがちょっと気持ち悪かったので下記「おまけ」でsrcsetを利用したPNG→SVGの差し替え処理を追記してみました ご参考まで asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/KxOnXf9XCl
ツイート 23
ちなみに、こちらの「おまけ」に書いたスクリプトを使えば、srcset対応のブラウザに加えて、IE9~11とAndroid3+の標準ブラウザでもSVGを適用できます http://t.co/KxOnXf9XCl https://t.co/1ftQqP6Pml
ツイート 24
RT @asamuzakjp: ちなみに、こちらの「おまけ」に書いたスクリプトを使えば、srcset対応のブラウザに加えて、IE9~11とAndroid3+の標準ブラウザでもSVGを適用できます http://t.co/KxOnXf9XCl https://t.co/1ftQ…
ツイート 25
もしかして、Safari9でも https://t.co/9QlrepKBra のFixが適用されていないの?orz asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/KxOnXf9XCl
ツイート 26
iOS9のSafariで確認、やっぱまだfixされてなかった…orz asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/KxOnXf9XCl
ツイート 27
使えば爽快…じゃなくて、 使えれば爽快…でもなくてそれどころか、 使えてもあぁそうかい? ってな感じだな…orz asamuzaK.jp : 使えば爽快! SVG Sprite http://t.co/KxOnXf9XCl
ツイート 28
なるほど…… asamuzaK.jp : 使えば爽快! SVG Sprite https://t.co/kPElZtDtvf
ツイート 29
RT @oomiyasr: なるほど…… asamuzaK.jp : 使えば爽快! SVG Sprite https://t.co/kPElZtDtvf
ツイート 30
お、去年11月にChromeもFixされていた♪ https://t.co/gZpbhYMGaE asamuzaK.jp : 使えば爽快! SVG Sprite https://t.co/KxOnXf9XCl
ツイート 31
webkitの振り分けは @media screen and (-webkit-min-device-pixel-ratio: 1) not (min-resolution: 1ddpx) { } で行けるかな?あとでテストしよう https://t.co/KxOnXf9XCl
ツイート 32
Chromeの変更を反映して、CSSでの指定方法のサンプルを書き直した しっかし、編集(追記)だらけでかなり読みにくくなってるな…orz asamuzaK.jp : 使えば爽快! SVG Sprite https://t.co/KxOnXf9XCl
ツイート 33
svgSpriteFromSrcset SVG#fragmentにバグがあるSafariには逆にsrcsetを適用させないようスクリプトを作り替えた asamuzaK.jp : 使えば爽快! SVG Sprite https://t.co/KxOnXf9XCl
ツイート 34
SVGスプライトについてあらためて追試中 <img src="a.svg" srcset="sprite.svg#a" /> <img src="b.svg" srcset="sprite.svg#b" /> https://t.co/KxOnXf9XCl
ツイート 35
このような場合 Chrome: sprite.svgへのリクエストは1回 Firefox, Edge, IE: sprite.svgへのリクエストが2回発生…orz https://t.co/KxOnXf9XCl
ツイート 36
CSSのbackgroundで使った場合も同様… Chromeは1回のリクエストであとはキャッシュから使いまわし Firefox, Edge, IEはフラグメント識別子で参照している数だけリクエストを発行している https://t.co/KxOnXf9XCl
ツイート 37
RT @asamuzakjp: このような場合 Chrome: sprite.svgへのリクエストは1回 Firefox, Edge, IE: sprite.svgへのリクエストが2回発生…orz https://t.co/KxOnXf9XCl
ツイート 38
RT @asamuzakjp: SVGスプライトについてあらためて追試中 <img src="a.svg" srcset="sprite.svg#a" /> <img src="b.svg" srcset="sprite.svg#b" /> https://t.co/KxOnX…
ツイート 39
RT @asamuzakjp: SVGスプライトについてあらためて追試中 <img src="a.svg" srcset="sprite.svg#a" /> <img src="b.svg" srcset="sprite.svg#b" /> https://t.co/KxOnX…
ツイート 40
.@ksk1015 Chromeも49からCSS backgroundで使えるようになっています ご参考まで: https://t.co/KxOnXf9XCl
ツイート 41
asamuzakjp: .ksk1015 Chromeも49からCSS backgroundで使えるようになっています ご参考まで: https://t.co/fW44kf4sOb
ツイート 42
_kamerontanseli: asamuzakjp: .ksk1015 Chromeも49からCSS backgroundで使えるようになっています ご参考まで: https://t.co/fW44kf4sOb
ツイート 43
SVGスプライト使ってませんか? Safariの実装にはバグがあります ご参考まで asamuzaK.jp : 使えば爽快! SVG Sprite https://t.co/KxOnXf9XCl https://t.co/274Xq47YhA
ツイート 44
ということで、こちらのページの「ポイント」部分も書き直した asamuzaK.jp : 使えば爽快! SVG Sprite https://t.co/KxOnXf9XCl
ツイート 45
Safariが9以降、再びSVG fragment identifiersを無効化した(実装をやめた?)みたいなので追記 この記事での手法はほとんど無意味です…orz asamuzaK.jp : 使えば爽快! SVG Sprite https://t.co/KxOnXf9XCl