使えば爽快! 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を追加するだけで良く、そもそもwxも要らない(ベクター画像なので)。

そこで、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 BugzillaMicrosoft 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);
})();

srcset属性からSVGを見つけて適用させるテスト

"使えば爽快! SVG Sprite"へのTwitter上でのコメントやRT

45件のツイートがあります。

ツイート 1

ツイート 2

ツイート 3

ツイート 4

ツイート 5

ツイート 6

ツイート 7

ツイート 8

ツイート 9

ツイート 10

ツイート 11

ツイート 12

ツイート 13

ツイート 14

ツイート 15

ツイート 16

ツイート 17

ツイート 18

ツイート 19

ツイート 20

ツイート 21

ツイート 22

ツイート 23

ツイート 24

ツイート 25

ツイート 26

ツイート 27

ツイート 28

ツイート 29

ツイート 30

ツイート 31

ツイート 32

ツイート 33

ツイート 34

ツイート 35

ツイート 36

ツイート 37

ツイート 38

ツイート 39

ツイート 40

ツイート 41

ツイート 42

ツイート 43

ツイート 44

ツイート 45