一般的にアイコンにフォントを使うのはあまりよろしくないとされてきました。なぜならサイズや位置調整が難しく、プロキシブラウザー、CORS(Cross-Origin Resource Sharing)などで変なエラーが出たり、アイコンの意味付けが難しく分かりにくいなどなど…、デメリットを挙げればキリがないからです。「CSS-Tricks」にも詳しく書かれています。
そうはいっても、SVGに対してもいつも使いにくいイメージを持っていました。正直に言えば、Sara Soueidan氏とは違う意見で、SVGのパス、変なタグ、属性などをどうやって使うかまったく分かりません。それでも、使わなければいけないときはあるでしょう。何より正しく使いこなすことに興味はあります。
そこで、シンプルかつ自動的にアイコンの作成ができるSVGのワークフローを考えました。公開レベルにするまでに少し時間がかかってしまいましたが、今回こうして記事にできるのはとても嬉しいことです。手前味噌ですが、かなり上手く機能していると思いますので、お役に立てれば幸いです。
要点をまとめると…
SVGスプライトでアイコンシステムを構築する方法について、「CSS-Tricks」のライターChris Coyier氏がとても分かりやすい記事を書いています。今回紹介する方法は彼のノウハウのいわば「応用編」なので、まだ読んだことがない人はぜひご一読を。
彼の記事の内容を簡単にまとめてみました。
- ソースアイコンは指定のフォルダーにSVGファイルとして個別に集められる。
- スプライトはsprite.shから生成されている。
- 次回以降の参考としてスプライトはメインのレイアウトに含まれている。
- アイコンは小さいコンポーネントを通じて表示される。
- ???
- 完成
セットアップはプロジェクト(Jekyll、 React、Railsなど…) によって少し異なります。しかし、Gist自体は変わりません。今回は攻略方法を紹介しますので、今日から使えます。
アイコンファイルを集める
何が一番難しいかと聞かれれば、実際に使えるSVGファイルが手元にあるかどうかかもしれません。アイコンをエクスポートするのに、どのツールを使うかによってマークアップも変わってきますし、不必要なコードで膨れ上がってしまうこともあります。そして、viewBoxについては、ややこしいので聞かないでください。
アイコンは、なるべく自作しないほうが良いと思います。Icomoonではたくさんの素敵なアイコンがフリーで配布されています。しかもSVGでのエクスポートにも対応しています。こんなに素晴らしいことはないでしょう。というわけで、今回はこれを使って説明します。
欲しいアイコンを選択して、「Generate SVG & More」をクリックして保存します。選んだアイコンは一覧されているので、まとめてダウンロードしましょう。ダウンロードしたzipファイルには必要な形式がすべて揃っています(PNGファイル、SVGファイル、CSS、JavaScript、デモなど)。念のためSVGフォルダーのデータをコピーして、アイコンのフォルダ内にペーストしておくことをお勧めします。
作業に入る前に少しだけSVGファイルを整理しておきましょう。これ自体は必須というわけではないですが、SVGスプライトが不要なコードで膨張しないためにも、コードの整理はお勧めします。下が「火」のアイコンをIcomoonからダウンロードしたときのコードです。
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="16" height="16" viewBox="0 0 16 16">
<path d="M5.016 16c-1.066-2.219-0.498-3.49 0.321-4.688 0.897-1.312 1.129-2.61
1.129-2.61s0.706 0.917 0.423 2.352c1.246-1.387 1.482-3.598 1.293-4.445 2.817 1.969
4.021 6.232 2.399 9.392 8.631-4.883 2.147-12.19 1.018-13.013 0.376 0.823 0.448
2.216-0.313 2.893-1.287-4.879-4.468-5.879-4.468-5.879 0.376 2.516-1.364 5.268-3.042
7.324-0.059-1.003-0.122-1.696-0.649-2.656-0.118 1.823-1.511 3.309-1.889 5.135-0.511
2.473 0.383 4.284 3.777 6.197z"></path>
</svg>
見ての通り<path>にごちゃごちゃとかかりすぎています。XML定義、最初のコメント、doctype、SVGラッパーを削除しましょう。スプライトを参照しながらSVGを埋め込むためです。これらを削除すると以下のコンテンツが残ります。
<path d="M5.016 16c-1.066-2.219-0.498-3.49 0.321-4.688 0.897-1.312 1.129-2.61
1.129-2.61s0.706 0.917 0.423 2.352c1.246-1.387 1.482-3.598 1.293-4.445 2.817 1.969
4.021 6.232 2.399 9.392 8.631-4.883 2.147-12.19 1.018-13.013 0.376 0.823 0.448
2.216-0.313 2.893-1.287-4.879-4.468-5.879-4.468-5.879 0.376 2.516-1.364 5.268-3.042
7.324-0.059-1.003-0.122-1.696-0.649-2.656-0.118 1.823-1.511 3.309-1.889 5.135-0.511
2.473 0.383 4.284 3.777 6.197z"></path>
スプライトを生成する
どうしてIcomoonから直接生成したスプライト(symbol-defs.svg)を使わないのか疑問に思う人もいるかもしれません。実際は使えるのですが、できることなら使いたくないのです。Icomoonから生成されたスプライトを使いたくないのには、以下のような理由があります。
- Icomoonはいくつか必要のないもの(主に属性)も含むから。しかも、必ずしも関連しているものではありません(例:<title>の後のファイル名など)。
- スプライトに新しいアイコンを追加するたびに、Icomoonに戻ってダウンロードしたくないから。プロジェクト内にシステムを持つほうがラクです。
アイコンファイルからスプライトを生成する方法はいくつかありますが、多くはGruntやGulpなどのアセットパイプラインによるものです。Bashスクリプトでsprite.shを作ったのはこれが理由です。Bashスクリプトは必要なことだけ実行します。
注意:もし愛用しているスプライトジェネレーターがあるなら、そちらを使ってください。sprite.shはアイコンをまとめるだけにGulp/Gruntやその他のものを読み込まないためのちょっとしたオプションに過ぎません。
npmまたはgemでsprite.shをインストールできます(両方Bashスクリプトの薄いラッパーなので気にしないでください)。
npm install spritesh -g
アイコンの入ったフォルダ上でsprite.shを起動します。アイコンファイルをassets/images/iconsに保存し、_includesフォルダ内でスプライトを生成したい場合、以下のように実行します。
spritesh --input assets/images/icons --output _includes/sprite.svg --viewbox "0 0 16 16" --prefix icon-
注意:スプライト生成時に毎回タイピングする手間を省くために、npm script内にコマンドを用意できます。
<svg>要素とviewBox属性をソースファイルから削除したので、viewBoxの引数が必要となります。今回はIcomoonが最初に使っているのと同じ0 0 16 16を使うことにします。
prefixの引数は必ずしも必要ではありません。既存のid属性がスプライトを含んだときにDOM内で衝突するのを防ぐためですが、必須ではありません。しかし、アイコンのid属性の名前を変更する良い練習にはなるでしょう。
注意:Windowsの場合、git bashまたはCygwinでsprite.shを動作させる必要があります。
実行すると下のようなコードになっているはずです。
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id='icon-fire' viewBox='0 0 16 16'><path d="M5.016
16c-1.066-2.219-0.498-3.49 0.321-4.688 0.897-1.312 1.129-2.61
1.129-2.61s0.706 0.917 0.423 2.352c1.246-1.387 1.482-3.598
1.293-4.445 2.817 1.969 4.021 6.232 2.399 9.392 8.631-4.883
2.147-12.19 1.018-13.013 0.376 0.823 0.448 2.216-0.313
2.893-1.287-4.879-4.468-5.879-4.468-5.879 0.376 2.516-1.364
5.268-3.042 7.324-0.059-1.003-0.122-1.696-0.649-2.656-0.118
1.823-1.511 3.309-1.889 5.135-0.511 2.473 0.383
4.284 3.777 6.197z"></path></symbol>
<!-- Other <symbol>s… -->
</svg>
あとはスプライトをメインのレイアウトに加えるだけです。例えば、JekyllWebサイトの場合、_includes/フォルダ内でスプライトを生成してレイアウトファイルに追加するだけです。
{% include sprite.svg %}
アイコンのコンポーネントを作成
ここまでで、アイコンを集めてスプライトを生成できました。あともう一歩です。スプライト化したアイコンを便利に使うためにはセットアップが必要です。スプライトから一致するシンボルを参照するために、<svg>タグ内で<use>タグを使います(もっと高度なテクニックが知りたければCSS-Tricksの投稿をチェック!)。
<svg viewBox="0 0 16 16" class="icon icon-fire">
<use xlink:href="#icon-fire"></use>
</svg>
これでも機能します。でも、あまり使いやすくないうえに、長期的に見て管理に手間がかかります。また、クラスやviewBox属性を変更したい場合、プロジェクトを最初からやり直さなければならず、理想的ではありません。
そこで、パーシャル内で反復するマークアップを使って抽象化します。Jekyllでは次のようなコードになります。
<svg viewBox="0 0 16 16" class="icon icon-{{ include.icon }}">
<use xlink:href="#icon-{{ include.icon }}"></use>
</svg>
これを使うためにはパーシャルを入れてiconパラメーターに渡します。
{% include icon.html icon="fire" %}
追加のクラスなどほかのパラメーターを受け入れるために、パーシャルは自由に改善してください。
Reactするとこんな感じです。
const Icon = (props) => (
<svg viewBox='0 0 16 16' className={`icon icon-${props.icon}`}>
<use xlinkHref={`#icon-${props.icon}`} />
</svg>
);
注意:xlinkHrefはReact 0.14.のみ有効です。React 0.13ではdangerouslySetInnerHTMLを使う必要があります。詳しくはStack Overflowを参照してください。
そして、以下を入力してください。
<Icon icon='fire' />
アクセシビリティに関してのアドバイス
Léonie Watson氏が書いた記事では、アクセシビリティ向上のため、スプライト内でタイトルと、<title>、<desc>それぞれの説明を<symbol>定義に加えることを勧めています。
この意見には同感ですが、タイトルと説明はコンテクストによってかなり変わってくると感じています。なので、個人的にタイトルと説明は、実際に使用する時点(コンポーネント内)で定義したほうが良いと思います。
例えば、アイコンをテキストと並べる場合、タイトルを強調したくはありません。なぜなら、すでにテキストがあるからです。一方、アイコンをコンテンツのボタンのみとして使う場合、ユーザーがアイコンの意味を理解できるようにタイトルと説明があると良いかもしれません。
アクセシビリティを考慮してタイトルと説明を省いた結果は、次のようになります。
{% capture id %}{% increment uniqueid %}{% endcapture %}
<svg viewBox="0 0 16 16" role="img" class="icon icon-{{ include.icon }}"
aria-labelledby="{% if include.title %}title-{{ id }}{% endif %}{% if include.desc %} desc-{{ id }}{% endif %}">
{% if include.title %}
<title id="title-{{ id }}">{{ include.title }}</title>
{% endif %}
{% if include.desc %}
<desc id="title-{{ id }}">{{ include.desc }}</desc>
{% endif %}
<use xlink:href="#icon-{{ include.icon }}"></use>
</svg>
{% increment %}は変数を初期化して、呼び出すたびに値を1ずつ加算します。つまり、アイコンパーシャルを加えるたび、呼び出されるアイコンが1つずつ増えて、最終的にはすべてのアイコンが呼び出されることとなります。
Reactバージョンも同じように機能します。ユニークidを取得するためにLodashを使っています(好みで使っても良いと思います)。
import { uniqueId } from 'lodash';
const Icon = (props) => {
const id = uniqueId();
return (
<svg viewBox='0 0 16 16' role='img'
className={`icon icon-${props.icon}`}
aria-labelledby={
(props.title ? `title-${id}` : '') +
(props.desc ? ` desc-${id}` : '')
}>
{props.title && <title id={`title-${id}`}>{props.title}</title>}
{props.desc && <desc id={`desc-${id}`}>{props.desc}</desc>}
<use xlinkHref={`#icon-${props.icon}`} />
</svg>
);
}
export default Icon;
かなり長ったらしいのは否めませんが、以下の理由で問題にはなりません。
- コンポーネントの役割は複雑さを抽象化することと反復を避けるためだから
- アクセシビリティが最も大切で、一番優先されるべきことだから
まとめ
いかがでしたか? まとめると、紹介したシステムは次のようなことが簡単にできるようになります。
- コマンドラインからカスタムオプションでスプライトを生成できる(構築されたどんなスクリプトにも対応が簡単となる)
- スプライトを使い、パーシャル/コンポーネントでアウトプットのカスタマイズができる
- 新しいアイコンを追加できる
素晴らしいですね! さらにもっと良くするには、SVGファイルを最適化するためにSVGOへパイプすることです。以下をnpm経由でインストールしましょう。
npm install svgo spritesh --save-dev
それから、npmスクリプトをpackage.jsonで最適化しましょう。
{
"scripts": {
"sprite": "spritesh --input assets/images/icons --output _includes/sprite.svg --viewbox '0 0 16 16' --prefix icon-",
"presprite": "svgo assets/images/icons"
},
"devDependencies": {
"spritesh": "^1.0.8",
"svgo": "^0.6.1"
}
}
これで、spriteタスクを作動させるたびに、npmが最初にsvgoをアイコンフォルダーで作動させます(最初の1回だけ便利かもしれませんが、新しいアイコンを追加することを考えたら保存しておくほうがベターです)。
SVGのテクニカルな説明をしてくれたSara Soueidan氏と、アクセシビリティについての詳しい説明をしてくれたHeydon Pickering氏に心から感謝します。