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コンポーネント
<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.呼び出し元
<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>
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を指定することで、指定した色でくり抜くことができます。
3-2-2. mask-contain
mask-contain
はTailwind CSSに用意されたユーティリティクラスで、CSSプロパティのmask-size: contain;
を指定しています。
mask-sizeはマスク画像の寸法を指定するプロパティで、background-size
と同じように指定することができます。
containでは画像のアスペクト比を維持したまま要素いっぱいに拡大してくれます。そのため、divタグに指定したサイズに合わせて拡大・縮小することができます。
3-2-3.mask-no-repeat
mask-no-repeat
は同じくTailwind CSSに用意されたユーティリティクラスで、CSSプロパティのmask-repeat: no-repeat;
を指定しています。こちらもbackground-repeat: no-repeat
と同様に画像表示を繰り返さないために指定しています。
3-2-4.mask-center
mask-center
は同じくTailwind CSSに用意されたユーティリティクラスで、CSSプロパティのmask-position: center;
を指定しています。こちらもbackground-position: center
と同様にマスク位置を要素の中心に指定しています。
おわりに
mask-imageを使用したIconコンポーネント作り方について紹介してきました。
SVGに変更があったりSVGのファイル名が変わったりした時には、public/icons
ディレクトリ直下に配置したSVGファイルの差し替えと、IconTypeの修正だけで済むので比較的管理しやすいと感じています。
また、SVGファイルにfillが定義されていた場合もbackground-colorでくり抜くため、実装者はSVGを直接編集する必要がなく比較的管理しやすいと感じています。
Iconコンポーネントを実装する際のアイデアの一つとしてお役に立つことができれば幸いです。

ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion