SVGアイコンをコンポーネント化して管理する方法【4つのアプローチ比較】
Webサイトを作っていると、SVGアイコンの管理がどんどん散らかっていく…ということがありました。imgタグで読み込んだり、インラインで直書きしたり、CSSのbackground-imageで指定したり。プロジェクトが大きくなるにつれて、同じアイコンが複数箇所にコピペされていたり、色やサイズの変更が一括でできなかったりするんですよね。
今回は、SVGアイコンをきちんとコンポーネント化して管理するための4つのアプローチを試してみました。それぞれメリット・デメリットがあるので、プロジェクトの規模や技術スタックに合わせて選ぶのが良さそうです。
目次
SVGアイコン管理が散らかる原因
まず、なぜSVGアイコンの管理は散らかりがちなのか整理してみます。
- コピペの増殖:同じSVGコードが複数ファイルに散らばる
- 色・サイズの統一が困難:ハードコードされたfillやwidth/heightが各所にある
- 読み込み方法がバラバラ:img、inline、CSS backgroundが混在する
- 更新が大変:アイコンを差し替えたいとき、全ファイルを探して回る必要がある
こうした問題を解決するために、アイコンを一元管理する仕組みが必要になります。では具体的な方法を見ていきます。
方法1:SVGスプライト(symbol + use)
昔からある定番の方法がSVGスプライトです。複数のSVGアイコンを1つのファイルにまとめて、<use>タグで参照します。
まず、スプライトファイルを作成します。
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5z"/>
</symbol>
<symbol id="icon-user" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</symbol>
</svg>
使う側では<use>タグでIDを指定するだけです。
<svg class="icon" width="24" height="24">
<use href="icons.svg#icon-home"></use>
</svg>
<svg class="icon" width="24" height="24">
<use href="icons.svg#icon-search"></use>
</svg>
メリット:HTTPリクエストが1回で済む、HTMLがシンプルになる、キャッシュが効く。
デメリット:外部ファイル参照だとCSSでの色変更に制約がある、スプライトファイルの管理が手動だと面倒。
方法2:インラインSVG直書き
もっともシンプルな方法が、SVGコードをHTMLに直接書く方法です。
<button class="btn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5z"/>
</svg>
検索
</button>
メリット:追加のHTTPリクエストが不要、CSSで色やサイズを自由に変更できる、アニメーションも簡単。
デメリット:HTMLが冗長になる、同じアイコンを複数箇所で使うとコードが重複する。
小規模なプロジェクトや、1箇所でしか使わないアイコンならこれで十分です。ただし、プロジェクトが大きくなると管理が厳しくなりますね。
方法3:Reactコンポーネント化(SVGR)
ReactやNext.jsを使っているなら、SVGRがめちゃくちゃ便利です。SVGファイルを自動的にReactコンポーネントに変換してくれます。
まずインストールします。
npm install --save-dev @svgr/webpack
# または Vite の場合
npm install --save-dev vite-plugin-svgr
SVGRを導入すると、SVGファイルをそのままimportしてコンポーネントとして使えます。
import HomeIcon from './icons/home.svg?react';
import SearchIcon from './icons/search.svg?react';
import UserIcon from './icons/user.svg?react';
function Navigation() {
return (
<nav>
<HomeIcon width={24} height={24} fill="currentColor" />
<SearchIcon width={24} height={24} fill="currentColor" />
<UserIcon width={24} height={24} fill="currentColor" />
</nav>
);
}
さらに、汎用的なIconコンポーネントを作っておくと便利です。
import HomeIcon from './icons/home.svg?react';
import SearchIcon from './icons/search.svg?react';
import UserIcon from './icons/user.svg?react';
import SettingsIcon from './icons/settings.svg?react';
const icons = {
home: HomeIcon,
search: SearchIcon,
user: UserIcon,
settings: SettingsIcon,
};
export function Icon({ name, size = 24, color = 'currentColor', ...props }) {
const SvgIcon = icons[name];
if (!SvgIcon) return null;
return (
<SvgIcon
width={size}
height={size}
fill={color}
aria-hidden="true"
{...props}
/>
);
}
// 使い方
// <Icon name="home" size={32} color="#333" />
メリット:型安全(TypeScript対応)、propsで色・サイズを柔軟に制御できる、Tree Shakingで未使用アイコンが除外される。
デメリット:React環境が前提、ビルド設定が必要。
方法4:CSSのmask-imageで使う
フレームワークに依存しない方法として、CSSのmask-imageを使うアプローチがあります。SVGファイルをマスクとして適用し、背景色でアイコンの色を制御します。
.icon {
display: inline-block;
width: 24px;
height: 24px;
background-color: currentColor;
mask-image: var(--icon-url);
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-image: var(--icon-url);
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
}
.icon-home {
--icon-url: url('/icons/home.svg');
}
.icon-search {
--icon-url: url('/icons/search.svg');
}
.icon-user {
--icon-url: url('/icons/user.svg');
}
<span class="icon icon-home"></span>
<span class="icon icon-search" style="color: red"></span>
メリット:フレームワーク不要、CSSだけで色の制御が可能、HTMLがシンプル。
デメリット:多色のSVGには使えない(単色のみ)、古いブラウザではベンダープレフィックスが必要。
サイズと色の制御テクニック
どの方法を使うにしても、サイズと色の制御は共通して重要なポイントです。
currentColorで色を親要素に委ねる
SVGのfillやstrokeにcurrentColorを指定すると、CSSのcolorプロパティで色を制御できるようになります。これが一番柔軟な方法ですね。
<!-- SVG側で currentColor を指定 -->
<svg fill="currentColor" viewBox="0 0 24 24">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
/* CSS側で color を変えるだけでアイコンの色が変わる */
.nav-link {
color: #666;
}
.nav-link:hover {
color: #333;
}
.nav-link.active {
color: #0066ff;
}
サイズの制御
SVGのサイズはwidthとheight属性、またはCSSで制御します。viewBoxが正しく設定されていれば、アスペクト比を保ったままリサイズされます。
/* CSSでサイズを統一管理する例 */
.icon {
width: 1em;
height: 1em;
}
.icon-sm { width: 16px; height: 16px; }
.icon-md { width: 24px; height: 24px; }
.icon-lg { width: 32px; height: 32px; }
.icon-xl { width: 48px; height: 48px; }
width: 1emにしておくと、フォントサイズに連動してアイコンサイズが変わるので、テキストと並べるときに相性が良いです。
アクセシビリティへの対応
SVGアイコンのアクセシビリティは見落とされがちですけど、きちんと対応しておくのが大事です。
装飾目的のアイコン
テキストと一緒に表示されるアイコン(テキストのラベルがある場合)は装飾扱いにします。
<!-- 装飾目的:スクリーンリーダーに無視させる -->
<button>
<svg aria-hidden="true" focusable="false" width="20" height="20">
<use href="icons.svg#icon-search"></use>
</svg>
検索
</button>
意味を持つアイコン
テキストなしでアイコンだけで意味を伝える場合は、roleとaria-labelを付けます。
<!-- 意味を持つアイコン:代替テキストを提供する -->
<button>
<svg role="img" aria-label="検索" width="20" height="20">
<use href="icons.svg#icon-search"></use>
</svg>
</button>
<!-- または title 要素を使う方法 -->
<button>
<svg role="img" aria-labelledby="search-title" width="20" height="20">
<title id="search-title">検索</title>
<use href="icons.svg#icon-search"></use>
</svg>
</button>
Reactコンポーネントの場合は、propsで切り替えられるようにしておくと便利ですね。
export function Icon({ name, size = 24, label, ...props }) {
const SvgIcon = icons[name];
if (!SvgIcon) return null;
const a11yProps = label
? { role: 'img', 'aria-label': label }
: { 'aria-hidden': true, focusable: false };
return <SvgIcon width={size} height={size} {...a11yProps} {...props} />;
}
// 装飾アイコン(テキストありボタン)
<Icon name="search" />
// 意味を持つアイコン(アイコンのみボタン)
<Icon name="search" label="検索" />
Viteでの自動インポート設定
Viteを使っているなら、vite-plugin-svgrを入れるだけでSVGをReactコンポーネントとして扱えます。
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
export default defineConfig({
plugins: [
react(),
svgr({
svgrOptions: {
// アイコンの属性を自動変換するオプション
icon: true, // viewBox を自動付与
svgo: true, // SVG最適化を有効化
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false, // viewBox は残す
},
},
},
'removeDimensions', // width/height を除去(CSSで制御)
],
},
// currentColor に自動変換
replaceAttrValues: {
'#000': 'currentColor',
'#000000': 'currentColor',
black: 'currentColor',
},
},
}),
],
});
この設定で以下のことが自動的に行われます。
- SVGOによる最適化(不要なメタデータの除去など)
viewBoxの自動付与width/height属性の除去(CSSで制御するため)- 黒色(
#000やblack)をcurrentColorに自動変換
TypeScriptを使っている場合は、型定義ファイルも追加しておきます。
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
実践:アイコンセットの構築ワークフロー
最後に、実際のプロジェクトでアイコンセットを構築するワークフローをまとめてみました。
1. ディレクトリ構成
src/
├── components/
│ └── Icon/
│ ├── Icon.tsx # Iconコンポーネント
│ ├── Icon.stories.tsx # Storybook用
│ └── index.ts # エクスポート
├── icons/
│ ├── navigation/
│ │ ├── home.svg
│ │ ├── menu.svg
│ │ └── arrow-back.svg
│ ├── action/
│ │ ├── search.svg
│ │ ├── delete.svg
│ │ └── edit.svg
│ └── social/
│ ├── twitter.svg
│ ├── github.svg
│ └── facebook.svg
└── styles/
└── icons.css # アイコン共通スタイル
アイコンをカテゴリ別にフォルダ分けしておくと、数が増えても見通しが良いです。
2. SVGファイルの準備ルール
チームで開発する場合、SVGファイルの命名と中身のルールを決めておくとトラブルが減ります。
- ファイル名はケバブケース(例:
arrow-right.svg) viewBoxは必ず設定する(width/height属性は不要)- 色は
currentColorにする(特定の色をハードコードしない) - 不要なメタデータ(エディタ情報など)は除去する
- 1つのSVGファイルに1つのアイコン
3. 自動インデックス生成スクリプト
アイコンが増えるたびに手動でimportを追加するのは面倒なので、スクリプトで自動化してみました。
import fs from 'fs';
import path from 'path';
const ICONS_DIR = './src/icons';
const OUTPUT_FILE = './src/components/Icon/iconMap.ts';
function toPascalCase(str) {
return str
.split('-')
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
.join('');
}
function scanIcons(dir, prefix = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const icons = [];
for (const entry of entries) {
if (entry.isDirectory()) {
icons.push(...scanIcons(path.join(dir, entry.name), entry.name));
} else if (entry.name.endsWith('.svg')) {
const name = entry.name.replace('.svg', '');
const importName = toPascalCase(name) + 'Icon';
const relativePath = prefix
? `../../icons/${prefix}/${entry.name}`
: `../../icons/${entry.name}`;
icons.push({ name, importName, relativePath });
}
}
return icons;
}
const icons = scanIcons(ICONS_DIR);
const imports = icons
.map(i => `import ${i.importName} from '${i.relativePath}?react';`)
.join('\n');
const map = icons
.map(i => ` '${i.name}': ${i.importName},`)
.join('\n');
const output = `${imports}
export const iconMap = {
${map}
} as const;
export type IconName = keyof typeof iconMap;
`;
fs.writeFileSync(OUTPUT_FILE, output);
console.log(`Generated icon map with ${icons.length} icons.`);
package.jsonにスクリプトを追加しておけば、アイコン追加時に1コマンドで反映できます。
{
"scripts": {
"icons": "node scripts/generate-icon-index.js",
"dev": "npm run icons && vite"
}
}
4. Storybookでカタログ化
Storybookを使って全アイコンをカタログ化しておくと、デザイナーや他の開発者が一覧で確認できます。これは地味ですけど、チーム開発では非常に役立ちますね。
4つのアプローチ比較まとめ
ここまで紹介した4つの方法を比較すると、以下のようになります。
- SVGスプライト:フレームワーク不要で使えるが、色の制御に制約あり。静的サイト向き
- インラインSVG:最もシンプルだが、コードの重複が発生しやすい。小規模向き
- SVGR(Reactコンポーネント):型安全でpropsによる柔軟な制御が可能。React/Next.jsプロジェクト向き
- CSS mask-image:CSSだけで完結、HTMLがシンプル。単色アイコン限定だがフレームワーク不問
個人的なおすすめは、React系のプロジェクトならSVGR、それ以外ならCSS mask-imageですね。どちらもcurrentColorで色を制御でき、管理の手間が少ないです。
まとめ
SVGアイコンの管理は、最初のうちは何とかなっていても、プロジェクトが成長するにつれて破綻しやすいです。早い段階でコンポーネント化の仕組みを入れておくと、後から楽になりますね。
- SVGファイルの命名・構成ルールを決める
currentColorで色を親要素に委ねる設計にする- アクセシビリティ属性を忘れずに付ける
- プロジェクトの技術スタックに合った方法を選ぶ
アイコンの管理がきれいに整理されると、開発体験がぐっと良くなります。
今回は以上です!