🍕

SvgIconコンポーネントでアイコンを一括管理する

に公開

概要

Vue.jsでSVGを表示するコンポーネントを作成する際に、アイコンごとにコンポーネントを作成するとアイコンの数が増えた時にコンポーネントの管理コストも高くなってしまうため、以下のように1つの汎用的なコンポーネントでまとめて扱える仕組みがあると便利です。

<SvgIcon icon-type="hoge" :size="sm" class="bg-red-500"/>
<SvgIcon icon-type="foo" :size="md" class="bg-blue-500"/>

今回はCSSのmask-imageプロパティを使用して、上記のように実装した例を紹介します。

1.開発環境

ざっくりと開発環境は以下の通りです。

  • vite 6.2.0
  • TypeScript 5.7.2
  • Vue 3.5.13
  • Tailwind CSS 4.1.4

2.完成系

2-1.プロジェクト構成

プロジェクト構成は以下のようになっています。
public/icons/直下にデザイナーから受け取ったsvgを配置しています。
今回は仮にICON MONOのSVG画像を配置しました。

project-root/
├── public/
│   └── icons
│       ├── fork-spoon.svg
│       ├── hammer.svg
│       ├── ketchup.svg
│       └── pizza.svg
└── src/                     
    ├── components/  
    │   └── SvgIcon.vue      # アイコンコンポーネント
    └── App.vue              # 呼び出し元

2-2.SvgIconコンポーネント

src/components/SvgIcon.vue
<script setup lang="ts">
import { computed } from "vue";

type IconType = "fork-spoon" | "hammer" | "ketchup" | "pizza";

interface Props {
  iconType: IconType;
  size?: "sm" | "md" | "lg" | number;
}

const props = withDefaults(defineProps<Props>(), {
  size: "md",
});

const iconSize = computed(() => {
  if (typeof props.size === "string") {
    switch (props.size) {
      case "sm":
        return "size-4";
      case "md":
        return "size-6";
      case "lg":
        return "size-8";
      default:
        return "size-6";
    }
  }
  return "";
});
// 
const sizeStyle = computed(() => {
  if (typeof props.size === "number") {
    return {
      width: `${props.size}px`,
      height: `${props.size}px`,
    };
  }
  return {};
});
</script>
<template>
  <div
    :class="`${iconSize} mask-contain mask-no-repeat mask-center`"
    :style="{
      ...sizeStyle,
      maskImage: `url(/icons/${props.iconType}.svg)`
    }"
  />
</template>

2-3.呼び出し元

src/App.vue(呼び出し元)
<script setup lang="ts">
import SvgIcon from './components/SvgIcon.vue'
</script>
<template>
  <SvgIcon icon-type="fork-spoon"  size="sm" class="bg-red-500" />
  <SvgIcon icon-type="hammer" size="md" class="bg-blue-500"/>
  <SvgIcon icon-type="ketchup" size="lg" class="bg-green-500"/>
  <SvgIcon icon-type="pizza" :size="100" class="bg-purple-500"/>
</template>

3.解説

3-1.SvgIconコンポーネント

3-1-1.icon名の絞り込み

Iconコンポーネントから呼び出し可能なiconはLiteral TypesでSVGファイル名に制限しています。

type IconType = "fork-spoon" | "hammer" | "ketchup" | "pizza";

3-1-2.アイコンサイズの指定

sm,md,lgもしくはカスタムサイズを受け取れるようにしておきます。

抜粋
<script setup lang="ts">
// propsから取得した固定のsizeに応じてTailwindのクラスを分岐
const iconSize = computed(() => {
  if (typeof props.size === "string") {
    switch (props.size) {
      case "sm":
        return "size-4";
      case "md":
        return "size-6";
      case "lg":
        return "size-8";
      default:
        return "size-6";
    }
  }
  return "";
});
</script>
<template>
  <div
    :class="iconSize"
  />
</template>

ただし、Tailwind CSSでは動的にクラス名を生成することはできないため、苦肉の策でインラインスタイルを定義しています。。。

抜粋
<script setup lang="ts">
// 受け取ったカスタムサイズをインラインスタイル用に設定します。
const sizeStyle = computed(() => {
  if (typeof props.size === "number") {
    return {
      width: `${props.size}px`,
      height: `${props.size}px`,
    };
  }
  return {};
});
</script>
<template>
  <div
    :style="{
      ...sizeStyle,
    }"
  />
</template>

https://v2.tailwindcss.com/docs/just-in-time-mode#arbitrary-value-support

3-1-3mask-imageでSVGファイルを指定

SVGのパスは mask-image(この後紹介します) にインラインで指定します。サイズと同様に動的に制御するため、スタイルとして直接設定しています。

抜粋
  <div
    :class="`mask-contain mask-no-repeat mask-center`"
    :style="{
      maskImage: `url(/icons/${props.iconType}.svg)`
    }"
  />

3-2.mask-*プロパティについて

3-2-1.mask-image

mask-imageは要素のマスクレイヤーとして使用する画像を設定するためのCSSプロパティです。
urlに画像パスを指定し、要素にbackground-colorを指定することで、指定した色でくり抜くことができます。
https://developer.mozilla.org/ja/docs/Web/CSS/mask-image
https://zenn.dev/kagan/articles/cf3332462262f1

3-2-2. mask-contain

mask-containはTailwind CSSに用意されたユーティリティクラスで、CSSプロパティのmask-size: contain;を指定しています。
mask-sizeはマスク画像の寸法を指定するプロパティで、background-sizeと同じように指定することができます。
containでは画像のアスペクト比を維持したまま要素いっぱいに拡大してくれます。そのため、divタグに指定したサイズに合わせて拡大・縮小することができます。
https://developer.mozilla.org/ja/docs/Web/CSS/mask-size

3-2-3.mask-no-repeat

mask-no-repeatは同じくTailwind CSSに用意されたユーティリティクラスで、CSSプロパティのmask-repeat: no-repeat;を指定しています。こちらもbackground-repeat: no-repeatと同様に画像表示を繰り返さないために指定しています。
https://developer.mozilla.org/ja/docs/Web/CSS/mask-repeat

3-2-4.mask-center

mask-centerは同じくTailwind CSSに用意されたユーティリティクラスで、CSSプロパティのmask-position: center;を指定しています。こちらもbackground-position: centerと同様にマスク位置を要素の中心に指定しています。
https://developer.mozilla.org/ja/docs/Web/CSS/mask-position

おわりに

mask-imageを使用したIconコンポーネント作り方について紹介してきました。
SVGに変更があったりSVGのファイル名が変わったりした時には、public/iconsディレクトリ直下に配置したSVGファイルの差し替えと、IconTypeの修正だけで済むので比較的管理しやすいと感じています。
また、SVGファイルにfillが定義されていた場合もbackground-colorでくり抜くため、実装者はSVGを直接編集する必要がなく比較的管理しやすいと感じています。
Iconコンポーネントを実装する際のアイデアの一つとしてお役に立つことができれば幸いです。

chot Inc. tech blog

Discussion