SVGアイコンの扱い方

star
<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=checkid=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について調べているうちにsvgstoresvg-spriteを見つけたので、今後はこれらのプログラムを使うことになると思う。

以上がここまで調べたことだ。 とはいえこのブログを書くだけでもう1日近く費やしてしまったので、そろそろ作業に戻ろうと思う。