🎨

SVGに色を注入する方法あれこれ(Vue.js)

2024/12/07に公開

SVGにテキストカラーやテーマカラーを当てたいことがよくあります。
手法と制約についてまとめてみました。

外部ファイルとして読み込んだSVGには色注入ができない

まず、基本的にimgタグなどで読み込んだSVGファイルに外側から色を注入することはできません。
currentColorやCSS変数を使ってはどうか、と思いつくかもしれませんが、外部SVGはドキュメントとして独立しているためHTML側に定義した色設定は反映されません。

なのでこれは:

<template>
  <img src="/vue.svg" class="logo vue" alt="Vue logo" />
</template>

<style>
:root {
  --main-color-1: red;
  --main-color-2: green;
  --main-color-3: blue;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img"
  class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198">
  <path fill="var(--main-color-1)" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path>
  <path fill="var(--main-color-2)" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path>
  <path fill="var(--main-color-3)" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path>
</svg>

CSS変数が未設定扱いとなるので、こうなります。

真っ黒になったVueロゴ

外部SVGのまま着色する方法として、mask-imageを使うテクニックもあります。

https://zenn.dev/kagan/articles/cf3332462262f1

SVGをインライン化する

単色の場合はmask-imageでもいいのですが、詳細に色を制御したいとなると、やはりインライン化は外せません。

手作業でSVGファイルをVue SFCに書き換える

温かみのある手作業による変換です。結局この手段を一番よく使います…

例えばこうであれば

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img"
  class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198">
  <path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path>
  <path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path>
  <path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path>
</svg>

このように<template>タグで囲みます。

<template>
  <svg>
    <!-- 中略 -->
  </svg>
</template>

VSCodeで編集している場合は保存時にVolarのフォーマッタが整形してくれます。それが仇になり、巨大なSVGはフリーズして困ることがたまにありますね…。そういう場合、先にSVGOMGをかけたり、SVG内の埋め込み画像を小さくできないかチェックしています。どうにもならない場合はSave without formattingしています。

unplugin-icons

https://github.com/unplugin/unplugin-icons

直接SVGをimportする方法です。アイコン用のプラグインですがSVG画像全般に利用できます。
Viteを使っている場合は下記のように設定します。

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Icons from "unplugin-icons/vite";
import { promises as fs } from "node:fs";
import { FileSystemIconLoader } from "unplugin-icons/loaders";

export default defineConfig({
  plugins: [
    vue(),
    Icons({
      customCollections: {
        "my-iconset": FileSystemIconLoader(
          "./assets/icons",
          (svg) => svg.replace(/^<svg /, '<svg fill="currentColor" '),
        ),
      },
    }),
  ],
});

この状態で下記のようにimportすることができます。

import IconAccount from '~icons/my-iconset/account'

このプラグインの便利機能として、SVGに簡単な文字列置換をかけることができます。

(svg) => svg.replace(/^<svg /, '<svg fill="currentColor" ')

例えばこのような置換処理を入れることで、SVGタグ全てのfillにcurrentColorを指定することができます。

@egoist/tailwindcss-icons

単色であれば @egoist/tailwindcss-icons を使うとiconifyを簡単に使えて色も指定(mask-imageで実現)できます。

unplugin-icons同様、任意のSVGをロードして、一部を文字列置換する機能もあります。

色注入の手法

1. currentColorを使う

1色の注入だけでよければ、currentColorキーワードをまず検討します。

<template>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    aria-hidden="true"
    role="img"
    width="37.07"
    height="36"
    preserveAspectRatio="xMidYMid meet"
    viewBox="0 0 256 198"
  >
    <path
      fill="currentColor"
      d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"
    ></path>
    <path
      fill="currentColor"
      d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"
    ></path>
  </svg>
</template>
<VueIcon width="100" height="100" color="red" />

紅に染まったVueロゴ

これだけで、利用側のcolor(文字色)が適用されます。単色アイコンは文字色と揃えることも多いので大体これだけで間に合います。

2. CSS Variablesを使う

CSS Variablesも便利です。

:root {
  --main-color: #41B883;
}
<path fill="var(--main-color)" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path>

複数色の注入も可能になります。

個人的には、要素に直接CSS変数を当てるのではなく、一旦クラスを経由することが多いです。

<script setup lang="ts">
import VueIcon from "./components/VueIcon.vue";
</script>

<template>
  <VueIcon width="100" height="100" />
</template>

<style>
:root {
  --main-color-1: #3fb1e5;
  --main-color-2: #1a5899;
}
</style>
<template>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    aria-hidden="true"
    role="img"
    width="37.07"
    height="36"
    preserveAspectRatio="xMidYMid meet"
    viewBox="0 0 256 198"
  >
    <path
      fill="#41B883"
      d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"
      class="fill-a"
    ></path>
    <path
      fill="#35495E"
      d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"
      class="fill-b"
    ></path>
  </svg>
</template>

<style scoped>
.fill-a {
  fill: var(--main-color-1);
}

.fill-b {
  fill: var(--main-color-2);
}
</style>

青くなったVueロゴ

このようにクラスでfill, stroke属性を上書きすると、もともとSVGにセットされていた色指定を消さずに済みます。色の差し込みは一括置換で行うことが多いのですが、置換をミスった時に色指定が残っていれば手がかりにして修正することができます。

また、color-mixを使った色の計算を行う場合などもクラスに書いてしまった方が見やすいですね。

3. propsで流し込んでv-bind

もちろんSFCなのでpropsで流し込んでもいいですね。

<VueIcon width="100" height="100" color1="#3fb1e5" color2="#1a5899" />
<script setup lang="ts">
defineProps<{
  color1: string;
  color2: string;
}>();
</script>

<template>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    aria-hidden="true"
    role="img"
    width="37.07"
    height="36"
    preserveAspectRatio="xMidYMid meet"
    viewBox="0 0 256 198"
  >
    <path
      :fill="color1"
      d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"
    ></path>
    <path
      :fill="color2"
      d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"
    ></path>
  </svg>
</template>

tinycolor2等のカラーライブラリを使って色計算を行うことができるのがこの方法の利点です。

この手法が必要なケースがわずかにあります。Figmaでドロップシャドウを設定した要素をSVGエクスポートすると、このようなSVGになります。

<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
  <rect width="100" height="100" fill="#F0F0F0" />
  <g filter="url(#filter0_d_1_2)">
    <rect x="28" y="27" width="43" height="43" fill="white" />
    <rect x="28.5" y="27.5" width="42" height="42" stroke="black" />
  </g>
  <defs>
    <filter id="filter0_d_1_2" x="23.2" y="26.2" width="52.6" height="52.6" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
      <feFlood flood-opacity="0" result="BackgroundImageFix" />
      <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
      <feOffset dy="4" />
      <feGaussianBlur stdDeviation="2.4" />
      <feComposite in2="hardAlpha" operator="out" />
      <feColorMatrix type="matrix" values="0 0 0 0 0.294727 0 0 0 0 0.529818 0 0 0 0 1 0 0 0 1 0" />
      <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_2" />
      <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_2" result="shape" />
    </filter>
  </defs>
</svg>

このドロップシャドウに色を注入したいと思った時、feDropShadowで色が付けられていると思いきやfeColorMatrix行列処理で色変換が行われています。

これをそのまま色を注入したい場合は、注入色のRGB値を分解した上でv-bindで行列に埋め込みます。

<feColorMatrix
  type="matrix"
  :values="`0 0 0 0 ${shadow.r} 0 0 0 0 ${shadow.g} 0 0 0 0 ${shadow.b} 0 0 0 ${shadow.a} 0`"
/>

もしくは、単純にSVGをdrop-shadowを使う形に書き直してもいいかもしれません。

<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
  <rect width="100" height="100" fill="#F0F0F0" />
  <rect x="28" y="27" width="43" height="43" fill="white" stroke="black"
    filter="drop-shadow(0px 4px 2.4px rgba(75, 135, 255, 1))" />
</svg>

おまけ:パフォーマンスを測ってみよう

SVGアイコン程度であれば、パフォーマンスを測るほどでもないのですが、多量のSVGにリアルタイムに色注入をするなどの要件があると、やはり計測は欠かせません。

  1. SVG5000個にCSS Variablesで色注入

https://stackblitz.com/edit/vitejs-vite-zrkppz?file=src%2FApp.vue

  1. SVG5000個にpropsで色注入

https://stackblitz.com/edit/vitejs-vite-ianmyu?file=src%2FApp.vue

CSS Variablesの方が若干早いですかね。Chrome / Safariで大きなパフォーマンスの差もありませんでした。SVGはちょっとしたことでパフォーマンスが大幅劣化することがあるので、こまめに計測して慎重にUIを作っていきたいところです!

Studio Tech Blog

Discussion