SVGアイコンの扱い方
27 Jan 2024<img src="/2024/01/27/star-fill.svg" width="32" height="32" />
SVGアイコンをWebサイト上に表示したいと思ったとき、単純に<img>
タグを使えばSVGを画像として扱うことができる。
このようにブラウザが直接SVG形式をサポートするようになって久しいけれども、フロントエンドを主に扱っていないとなかなかSVGタグを頻繁に使う機会は訪れない。
というのもSVGには<svg>
という専用のタグがあって、このタグを使うことでもう少し直感的にSVGを扱うことができるようだ。
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="star" viewBox="0 0 16 16">
<path d="M3.612 ... 2.256z" />
</svg>
とりわけHTML内にSVGのデータそのものを貼り付けるという手法がある。
この方法を使うとclass
を指定したりfill
という要素が設定できるようになる。
よく見かけるのは<svg fill="currentColor" class="icon">
のように書けばicon
クラスで単色のファイルであれば既存の文字色にあわせることができる。
このように単純なアイコンであればいいのだが、素のHTMLでこのファイルを扱うには少々使い勝手が悪く感じる。
RailsなどでこういったSVGアイコンを扱おうと思うと単純にヘルパーを書けばよい:
module SvgHelper
def svg(svg_path)
raw Rails.root.join("app/assets/images", svg_path).read
end
end
このヘルパーで次のように書ける。
<%= svg "star-fill.svg" %>
star-fill.svg
がHTML上で直接展開されるようになるようだ。
ただしraw
ヘルパーを使うのでRubocopから警告が表示されるので、これもよくない。
よくないけれども、<img>
タグを使うよりは便利なのでこの方法を使っていた。
そして今日このブログを書こうと思いついたきっかけとしてはxlink:href
という要素を指定できることを知ったからだ。
厳密にはこの要素を知ったのは今日が初めてではないけれども、踏み込んで理解しようと思ったのが今日だった。
NOTE: ChatGPTによるとxlink:href
は古い属性なので、この投稿でもhref
を指定する。
<div style="display: none">
<svg xmlns="http://www.w3.org/2000/svg">
<path id="star-fill" d="M3.612 ... 2.256z"/>
</svg>
</div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="star" fill="currentColor">
<use href="#star-fill" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="star" fill="currentColor">
<use href="#star-fill" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="star" fill="currentColor">
<use href="#star-fill" />
</svg>
このコードはもとのSVGを表示する必要はあるが、1度定義してしまえば<use>
タグで何度も使いまわしができるようだ。
ただ残念ながら画像の高さと幅が指定できなくなってしまった。
HTMLで一度定義した箇所はCSSで隠す必要が出てくるので、昔のclearfixのテクニックとまではいわないまでもそれなら直接SVGをコードに埋め込む方法と一長一短な気がする。
しかしある方法を使えば直接SVGを指定できるようになるという:
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="star" viewBox="0 0 16 16">
<use href="/2024/01/27/defs-star-fill.svg#star-fill" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="star" viewBox="0 0 16 16">
<use href="/2024/01/27/defs-star-fill.svg#star-fill" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="star" viewBox="0 0 16 16">
<use href="/2024/01/27/defs-star-fill.svg#star-fill" />
</svg>
少なくとも私のブラウザの表示を見る限りでは真ん中の星が拡大されているように見える。
では何が変更されたのかというと:
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor">
<defs>
<path id="star-fill" d="M3.612 ... 2.256z"/>
</defs>
</svg>
defs-star-fill.svg
ファイルの中身だ。<defs>
タグを用意して、<path>
にid="star-fill"
を追加した。
あとはこのファイルを#star-fill
を指定して呼び出せばよい。
この方法はSVGがどんなに長くとも問題ないし、画像も任意のサイズに指定することができる。
ReactやVueをそもそも使わないでSVGを呼び出すケースがそもそもあまり多くないかもしれないのだが、実は<img>
タグのような使い勝手でなおかつCSSも加えることができるというと非常に便利である。
しかし今回は非常に単純なアイコンなのでパスも1つで済むのだけれども、現実には複数のパスを持つアイコンのほうが多い。
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 ... 16"/>
<path d="m10.97 ... 0-1.071-1.05"/>
</svg>
例えばこのアイコン自体は単純だが丸とチェックマークで構成されているのでパスが2つ存在する。
こういった場合<path>
タグは2つ存在するけれども先程のケースを見ると、それぞれid=check
とid=circle
を命名する必要があるのかというともちろんそのようなことはない。
ここで<g>
タグが登場する:
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<defs>
<g id="check-circle">
<path d="M8 ... 16"/>
<path d="m10.97 ... 0-1.071-1.05"/>
</g>
</defs>
</svg>
これでどんなにパスが多くても1つのファイルで参照できるようになった。
また<defs>
タグの中には複数の<path>
タグや<g>
タグを記載できる。
古のブラウザハックで存在したスプライト画像のようなテクニックに近いが、Rakeでこのようなコードを書いた。
namespace :svg do
desc "sidebar.svg"
task sidebar: :environment do
# svg本体を用意する
brand = Nokogiri::HTML.fragment <<-HTML.squish
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
</defs>
</svg>
HTML
brand_defs = brand.at("svg defs")
# svg画像をdefsに挿入する
images = %w(pencil speedometer star)
images.each do |image|
image_path = File.join("app/assets/images/", "#{image}.svg")
full = Nokogiri::HTML(File.open(image_path))
full_symbol = Nokogiri::XML::Node.new("g", brand)
full_symbol["id"] = image
full_symbol["fill"] = "currentColor"
full.xpath("//path").each do |path|
full_symbol.add_child(path)
end
brand_defs.add_child(full_symbol)
end
# svg画像を保存する
svg_path = "app/assets/images/sidebar.svg"
File.write(svg_path, brand.to_html(save_with: 0))
end
end
普段例示しているコードに比べるとかなり雑なコードだけれども、これはこれでもう使わないと思うが、NokogiriでDOM操作しているため残すことにしようと思った。
たまたまSVGについて調べているうちにsvgstoreとsvg-spriteを見つけたので、今後はこれらのプログラムを使うことになると思う。
以上がここまで調べたことだ。 とはいえこのブログを書くだけでもう1日近く費やしてしまったので、そろそろ作業に戻ろうと思う。