T-CREATOR

Vite で SVG アイコンをスマートに使いこなす

Vite で SVG アイコンをスマートに使いこなす

モダンな Web アプリケーション開発において、アイコンは単なる装飾要素を超えて、ユーザーエクスペリエンスの核心を担う重要な要素となっています。特に SVG(Scalable Vector Graphics)アイコンは、その柔軟性とパフォーマンスの優位性から、多くの開発者に選ばれています。

しかし、SVG アイコンを効率的に管理し、プロジェクト全体で一貫性を保ちながら活用するのは、意外と複雑な作業です。そんな課題を解決してくれるのが、Vite の強力な SVG 処理機能です。

今回は、Vite を使って SVG アイコンをスマートに活用する方法について、基本的な導入から実践的なテクニックまで詳しく解説していきます。この記事を読むことで、あなたのプロジェクトでも効率的で保守性の高い SVG アイコンシステムを構築できるようになるでしょう。

Web アプリケーションにおけるアイコンの役割

ユーザーインターフェースの重要な構成要素

現代の Web アプリケーションにおいて、アイコンは情報の視覚的な伝達手段として欠かせない存在です。ユーザーは直感的にアイコンの意味を理解し、効率的にアプリケーションを操作できます。

特に以下のような場面で、アイコンの重要性が際立ちます。

#用途効果
1ナビゲーション機能の素早い識別と操作性向上
2ステータス表示情報の視覚的な伝達
3アクション誘導ユーザーの行動促進
4ブランディング一貫性のあるデザイン体験

レスポンシブデザインでの柔軟性

モバイルファーストの時代において、アイコンは限られた画面スペースを有効活用するための重要なツールです。テキストラベルでは表現しきれない情報を、コンパクトで分かりやすい形で提供できます。

さらに、多言語対応のアプリケーションでは、言語に依存しないアイコンの価値がより一層高まります。文化的な背景を超えて、普遍的に理解される視覚的コミュニケーションが可能になるのです。

従来の画像形式の限界

PNG・JPEG の課題

従来の Web 開発では、アイコンに PNG や JPEG 形式の画像を使用することが一般的でした。しかし、これらの形式には以下のような制約があります。

typescript// 従来の画像アイコンの問題例
const IconComponent = () => {
  return (
    <img
      src='/icons/user.png'
      alt='ユーザー'
      width='24'
      height='24'
      // 高解像度ディスプレイでぼやける
      // 色の変更が困難
      // ファイルサイズが大きい
    />
  );
};

解像度とスケーラビリティの問題

ピクセルベースの画像は、異なる画面解像度や表示サイズに対応するために、複数のバリエーションを用意する必要があります。これにより、以下のような課題が発生します。

#課題影響
1ファイル数の増加管理コストの上昇
2品質の劣化高解像度ディスプレイでの表示問題
3読み込み時間パフォーマンスの低下
4キャッシュ効率複数ファイルによる非効率性

SVG が選ばれる理由

ベクターグラフィックスの優位性

SVG はベクターベースの画像形式であり、数学的な計算によって図形を描画します。これにより、どのようなサイズでも鮮明な表示が可能になります。

xml<!-- SVG の基本構造 -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
  <path
    d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"
    fill="currentColor"
  />
  <!-- ベクターデータなので無限にスケール可能 -->
</svg>

CSS との親和性

SVG アイコンは CSS で直接スタイリングできるため、動的な色変更やアニメーション効果を簡単に実装できます。

css/* SVG アイコンのスタイリング例 */
.icon {
  width: 1.5rem;
  height: 1.5rem;
  fill: var(--primary-color);
  transition: fill 0.2s ease;
}

.icon:hover {
  fill: var(--primary-color-hover);
  transform: scale(1.1);
}

/* ダークモード対応も簡単 */
@media (prefers-color-scheme: dark) {
  .icon {
    fill: var(--primary-color-dark);
  }
}

パフォーマンスの優位性

適切に最適化された SVG ファイルは、同等の PNG ファイルよりもファイルサイズが小さくなることが多く、HTTP リクエスト数の削減にも貢献します。

複数の SVG ファイル管理の煩雑さ

ファイル散在による管理困難

プロジェクトが成長するにつれて、SVG アイコンの数は急激に増加します。適切な管理システムがないと、以下のような問題が発生します。

arduinoproject/
├── assets/
│   ├── icons/
│   │   ├── user.svg
│   │   ├── settings.svg
│   │   ├── notification.svg
│   │   ├── user-profile.svg  // 重複?
│   │   ├── user_icon.svg     // 命名規則の不統一
│   │   └── ...100個以上のファイル
│   └── images/
└── components/
    ├── UserIcon.tsx    // どのSVGを使用?
    └── ProfileIcon.tsx // 同じアイコンの重複使用?

命名規則とバージョン管理

統一された命名規則がないと、開発チーム内でのコミュニケーションが困難になります。また、デザインの更新時にどのファイルが最新版なのか判断が難しくなります。

バンドルサイズの肥大化

未使用アイコンの混入

従来の方法では、実際に使用していない SVG ファイルもバンドルに含まれてしまうことがあります。これにより、アプリケーションの初期読み込み時間が延びてしまいます。

typescript// 問題のあるインポート例
import UserIcon from './icons/user.svg';
import SettingsIcon from './icons/settings.svg';
import NotificationIcon from './icons/notification.svg';
// ... 50個のアイコンをインポート
// しかし実際に使用するのは5個だけ

const MyComponent = () => {
  // 一部のアイコンしか使用しない
  return (
    <div>
      <UserIcon />
      <SettingsIcon />
    </div>
  );
};

Tree Shaking の効果的活用の困難

適切な設定がないと、Vite の Tree Shaking 機能が SVG ファイルに対して効果的に働かない場合があります。

動的な色変更・サイズ変更の困難

ハードコーディングされたスタイル

多くの SVG ファイルには、色やサイズがハードコーディングされています。これにより、テーマの変更やレスポンシブ対応が困難になります。

xml<!-- 問題のある SVG 例 -->
<svg width="24" height="24" viewBox="0 0 24 24">
  <path
    d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"
    fill="#3B82F6"  <!-- 固定色 -->
    stroke="#1E40AF" <!-- 固定色 -->
  />
</svg>

プロパティベースの制御の実現困難

React や Vue.js のコンポーネントとして使用する際、props による動的な制御が難しくなります。

Vite の SVG 処理機能

内蔵の SVG サポート

Vite は標準で SVG ファイルのインポートをサポートしており、追加の設定なしで基本的な SVG の使用が可能です。

typescript// Vite での基本的な SVG インポート
import iconUrl from './assets/icon.svg';

const BasicUsage = () => {
  return <img src={iconUrl} alt='アイコン' />;
};

URL インポートと Inline インポート

Vite では、SVG を URL として読み込む方法と、インライン要素として読み込む方法の両方をサポートしています。

typescript// URL としてインポート
import iconUrl from './assets/icon.svg?url';

// インライン要素としてインポート(要プラグイン)
import IconComponent from './assets/icon.svg?react';

const FlexibleUsage = () => {
  return (
    <div>
      {/* 画像として使用 */}
      <img src={iconUrl} alt='URL アイコン' />

      {/* コンポーネントとして使用 */}
      <IconComponent className='inline-icon' />
    </div>
  );
};

プラグインエコシステムの活用

vite-plugin-svgr の導入

React プロジェクトでは、vite-plugin-svgr を使用することで、SVG ファイルを React コンポーネントとして直接インポートできます。

bash# パッケージのインストール
yarn add -D vite-plugin-svgr
typescript// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';

export default defineConfig({
  plugins: [
    react(),
    svgr({
      // SVG を React コンポーネントとして使用
      svgrOptions: {
        exportType: 'default',
        ref: true,
        svgo: false,
        titleProp: true,
      },
      include: '**/*.svg',
    }),
  ],
});

unplugin-icons の活用

より高度なアイコン管理には、unplugin-icons を使用することをお勧めします。

bash# 必要なパッケージのインストール
yarn add -D unplugin-icons @iconify/json
typescript// vite.config.ts でのunplugin-icons設定
import Icons from 'unplugin-icons/vite';

export default defineConfig({
  plugins: [
    react(),
    Icons({
      // 自動インストール
      autoInstall: true,
      // コンパイラー設定
      compiler: 'jsx',
      jsx: 'react',
    }),
  ],
});

最適化手法の導入

SVGO による自動最適化

SVG ファイルの最適化には、SVGO(SVG Optimizer)を活用します。

bash# SVGO のインストール
yarn add -D vite-plugin-svgo
typescript// vite.config.ts
import { svgo } from 'vite-plugin-svgo';

export default defineConfig({
  plugins: [
    react(),
    svgo({
      plugins: [
        {
          name: 'preset-default',
          params: {
            overrides: {
              // viewBox を保持
              removeViewBox: false,
              // ID を保持(必要に応じて)
              cleanupIDs: false,
            },
          },
        },
      ],
    }),
  ],
});

Tree Shaking の最適化

使用していない SVG アイコンがバンドルに含まれないよう、適切な設定を行います。

typescript// アイコンの効率的なエクスポート
// icons/index.ts
export { default as UserIcon } from './user.svg?react';
export { default as SettingsIcon } from './settings.svg?react';
export { default as NotificationIcon } from './notification.svg?react';

// 使用側では必要なアイコンのみインポート
import { UserIcon, SettingsIcon } from '@/icons';

基本的な SVG インポート方法

ファイル構造の整理

効率的な SVG 管理のために、まずは適切なファイル構造を構築しましょう。

bashsrc/
├── assets/
│   └── icons/
│       ├── common/          # 共通アイコン
│       │   ├── user.svg
│       │   ├── settings.svg
│       │   └── notification.svg
│       ├── navigation/      # ナビゲーション専用
│       │   ├── home.svg
│       │   ├── search.svg
│       │   └── profile.svg
│       └── actions/         # アクション系
│           ├── edit.svg
│           ├── delete.svg
│           └── save.svg
├── components/
│   └── icons/
│       └── index.ts         # アイコンの統一エクスポート
└── types/
    └── svg.d.ts            # TypeScript型定義

TypeScript 型定義の設定

SVG ファイルを TypeScript で安全に使用するために、型定義を追加します。

typescript// types/svg.d.ts
declare module '*.svg' {
  import React = require('react');
  export const ReactComponent: React.FC<
    React.SVGProps<SVGSVGElement>
  >;
  const src: string;
  export default src;
}

declare module '*.svg?react' {
  import React = require('react');
  const ReactComponent: React.FC<
    React.SVGProps<SVGSVGElement>
  >;
  export default ReactComponent;
}

declare module '*.svg?url' {
  const content: string;
  export default content;
}

基本的なインポートパターン

Vite では複数の方法で SVG を読み込むことができます。用途に応じて適切な方法を選択しましょう。

typescript// 1. URL としてインポート(img要素で使用)
import userIconUrl from '@/assets/icons/common/user.svg?url';

// 2. React コンポーネントとしてインポート
import UserIcon from '@/assets/icons/common/user.svg?react';

// 3. インライン SVG として取得
import userIconRaw from '@/assets/icons/common/user.svg?raw';

const IconUsageExample = () => {
  return (
    <div className='icon-examples'>
      {/* URL を使用した表示 */}
      <img
        src={userIconUrl}
        alt='ユーザー'
        className='icon-url'
      />

      {/* React コンポーネントとして使用 */}
      <UserIcon
        className='icon-component'
        fill='currentColor'
        width={24}
        height={24}
      />

      {/* インライン SVG として使用 */}
      <div
        dangerouslySetInnerHTML={{ __html: userIconRaw }}
        className='icon-inline'
      />
    </div>
  );
};

vite-plugin-svgr を使ったコンポーネント化

詳細な設定オプション

vite-plugin-svgr では、SVG の変換方法を細かく制御できます。

typescript// vite.config.ts - 詳細設定
import svgr from 'vite-plugin-svgr';

export default defineConfig({
  plugins: [
    react(),
    svgr({
      svgrOptions: {
        exportType: 'default',
        ref: true,
        svgo: true,
        titleProp: true,
        descProp: true,
        // JSX の props を追加
        expandProps: 'end',
        // TypeScript サポート
        typescript: true,
        // カスタム属性の処理
        replaceAttrValues: {
          '#000': 'currentColor',
          '#000000': 'currentColor',
        },
      },
      include: '**/*.svg',
      exclude: '',
    }),
  ],
});

汎用アイコンコンポーネントの作成

再利用性を高めるため、共通のアイコンコンポーネントを作成します。

typescript// components/icons/Icon.tsx
import React from 'react';
import { SVGProps } from 'react';

// アイコンサイズの定義
export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

// アイコンコンポーネントのプロパティ
export interface IconProps
  extends Omit<SVGProps<SVGSVGElement>, 'size'> {
  size?: IconSize;
  color?: string;
}

// サイズマッピング
const sizeMap: Record<IconSize, number> = {
  xs: 12,
  sm: 16,
  md: 20,
  lg: 24,
  xl: 32,
};

// 汎用アイコンコンポーネント
export const Icon: React.FC<
  IconProps & {
    component: React.ComponentType<SVGProps<SVGSVGElement>>;
  }
> = ({
  component: Component,
  size = 'md',
  color = 'currentColor',
  className = '',
  ...props
}) => {
  const iconSize = sizeMap[size];

  return (
    <Component
      width={iconSize}
      height={iconSize}
      fill={color}
      className={`icon icon-${size} ${className}`}
      {...props}
    />
  );
};

個別アイコンコンポーネントの作成

各アイコンに対して、型安全で使いやすいコンポーネントを作成します。

typescript// components/icons/UserIcon.tsx
import React from 'react';
import UserSvg from '@/assets/icons/common/user.svg?react';
import { Icon, IconProps } from './Icon';

export const UserIcon: React.FC<IconProps> = (props) => {
  return <Icon component={UserSvg} {...props} />;
};

// components/icons/SettingsIcon.tsx
import React from 'react';
import SettingsSvg from '@/assets/icons/common/settings.svg?react';
import { Icon, IconProps } from './Icon';

export const SettingsIcon: React.FC<IconProps> = (
  props
) => {
  return <Icon component={SettingsSvg} {...props} />;
};

統一エクスポートファイルの作成

すべてのアイコンを一箇所から管理できるよう、統一エクスポートファイルを作成します。

typescript// components/icons/index.ts
export { Icon } from './Icon';
export type { IconProps, IconSize } from './Icon';

// 共通アイコン
export { UserIcon } from './UserIcon';
export { SettingsIcon } from './SettingsIcon';
export { NotificationIcon } from './NotificationIcon';

// ナビゲーションアイコン
export { HomeIcon } from './HomeIcon';
export { SearchIcon } from './SearchIcon';
export { ProfileIcon } from './ProfileIcon';

// アクションアイコン
export { EditIcon } from './EditIcon';
export { DeleteIcon } from './DeleteIcon';
export { SaveIcon } from './SaveIcon';

// アイコン名の型定義
export type IconName =
  | 'user'
  | 'settings'
  | 'notification'
  | 'home'
  | 'search'
  | 'profile'
  | 'edit'
  | 'delete'
  | 'save';

カスタムアイコンセットの構築

アイコンファクトリーパターンの実装

動的にアイコンを選択できるファクトリーパターンを実装します。

typescript// components/icons/IconFactory.tsx
import React from 'react';
import { IconProps } from './Icon';
import {
  UserIcon,
  SettingsIcon,
  NotificationIcon,
  HomeIcon,
  SearchIcon,
  ProfileIcon,
  EditIcon,
  DeleteIcon,
  SaveIcon,
} from './index';

// アイコンマッピング
const iconMap = {
  user: UserIcon,
  settings: SettingsIcon,
  notification: NotificationIcon,
  home: HomeIcon,
  search: SearchIcon,
  profile: ProfileIcon,
  edit: EditIcon,
  delete: DeleteIcon,
  save: SaveIcon,
} as const;

export type IconName = keyof typeof iconMap;

// アイコンファクトリーコンポーネント
export interface IconFactoryProps extends IconProps {
  name: IconName;
}

export const IconFactory: React.FC<IconFactoryProps> = ({
  name,
  ...props
}) => {
  const IconComponent = iconMap[name];

  if (!IconComponent) {
    console.warn(`アイコン "${name}" が見つかりません`);
    return null;
  }

  return <IconComponent {...props} />;
};

テーマ対応アイコンシステム

ライトモード・ダークモードに対応したアイコンシステムを構築します。

typescript// hooks/useTheme.ts
import { useContext } from 'react';
import { ThemeContext } from '@/contexts/ThemeContext';

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error(
      'useTheme は ThemeProvider 内で使用してください'
    );
  }
  return context;
};

// components/icons/ThemedIcon.tsx
import React from 'react';
import {
  IconFactory,
  IconFactoryProps,
} from './IconFactory';
import { useTheme } from '@/hooks/useTheme';

export const ThemedIcon: React.FC<IconFactoryProps> = ({
  color,
  className = '',
  ...props
}) => {
  const { theme } = useTheme();

  // テーマに応じた色の設定
  const themeColor =
    color || (theme === 'dark' ? '#ffffff' : '#000000');
  const themeClassName = `${className} icon-theme-${theme}`;

  return (
    <IconFactory
      color={themeColor}
      className={themeClassName}
      {...props}
    />
  );
};

アイコンプレビューコンポーネント

開発効率を向上させるため、すべてのアイコンを一覧できるプレビューコンポーネントを作成します。

typescript// components/icons/IconPreview.tsx
import React, { useState } from 'react';
import { IconFactory, IconName } from './IconFactory';
import { IconSize } from './Icon';

// 利用可能なアイコン一覧
const availableIcons: IconName[] = [
  'user',
  'settings',
  'notification',
  'home',
  'search',
  'profile',
  'edit',
  'delete',
  'save',
];

const availableSizes: IconSize[] = [
  'xs',
  'sm',
  'md',
  'lg',
  'xl',
];

export const IconPreview: React.FC = () => {
  const [selectedSize, setSelectedSize] =
    useState<IconSize>('md');
  const [selectedColor, setSelectedColor] =
    useState('#3B82F6');

  return (
    <div className='icon-preview'>
      <div className='preview-controls'>
        <div className='size-control'>
          <label>サイズ:</label>
          <select
            value={selectedSize}
            onChange={(e) =>
              setSelectedSize(e.target.value as IconSize)
            }
          >
            {availableSizes.map((size) => (
              <option key={size} value={size}>
                {size}
              </option>
            ))}
          </select>
        </div>

        <div className='color-control'>
          <label>色:</label>
          <input
            type='color'
            value={selectedColor}
            onChange={(e) =>
              setSelectedColor(e.target.value)
            }
          />
        </div>
      </div>

      <div className='icons-grid'>
        {availableIcons.map((iconName) => (
          <div key={iconName} className='icon-item'>
            <IconFactory
              name={iconName}
              size={selectedSize}
              color={selectedColor}
            />
            <span className='icon-name'>{iconName}</span>
          </div>
        ))}
      </div>
    </div>
  );
};

動的な色・サイズ変更の実装

CSS カスタムプロパティとの連携

CSS カスタムプロパティを活用することで、より柔軟なスタイリングが可能になります。

css/* styles/icons.css */
:root {
  /* アイコンのカラーパレット */
  --icon-primary: #3b82f6;
  --icon-secondary: #6b7280;
  --icon-success: #10b981;
  --icon-warning: #f59e0b;
  --icon-danger: #ef4444;

  /* アイコンサイズ */
  --icon-xs: 0.75rem;
  --icon-sm: 1rem;
  --icon-md: 1.25rem;
  --icon-lg: 1.5rem;
  --icon-xl: 2rem;
}

/* ダークモード対応 */
@media (prefers-color-scheme: dark) {
  :root {
    --icon-primary: #60a5fa;
    --icon-secondary: #9ca3af;
    --icon-success: #34d399;
    --icon-warning: #fbbf24;
    --icon-danger: #f87171;
  }
}

/* アイコンの基本スタイル */
.icon {
  display: inline-block;
  vertical-align: middle;
  fill: currentColor;
  transition: all 0.2s ease;
}

/* サイズクラス */
.icon-xs {
  width: var(--icon-xs);
  height: var(--icon-xs);
}
.icon-sm {
  width: var(--icon-sm);
  height: var(--icon-sm);
}
.icon-md {
  width: var(--icon-md);
  height: var(--icon-md);
}
.icon-lg {
  width: var(--icon-lg);
  height: var(--icon-lg);
}
.icon-xl {
  width: var(--icon-xl);
  height: var(--icon-xl);
}

/* カラークラス */
.icon-primary {
  color: var(--icon-primary);
}
.icon-secondary {
  color: var(--icon-secondary);
}
.icon-success {
  color: var(--icon-success);
}
.icon-warning {
  color: var(--icon-warning);
}
.icon-danger {
  color: var(--icon-danger);
}

アニメーション効果の実装

SVG アイコンにインタラクティブなアニメーション効果を追加します。

typescript// components/icons/AnimatedIcon.tsx
import React, { useState } from 'react';
import {
  IconFactory,
  IconFactoryProps,
} from './IconFactory';

export interface AnimatedIconProps
  extends IconFactoryProps {
  animation?:
    | 'hover-scale'
    | 'hover-rotate'
    | 'pulse'
    | 'bounce';
  duration?: number;
}

export const AnimatedIcon: React.FC<AnimatedIconProps> = ({
  animation = 'hover-scale',
  duration = 200,
  className = '',
  onMouseEnter,
  onMouseLeave,
  ...props
}) => {
  const [isHovered, setIsHovered] = useState(false);

  const handleMouseEnter = (
    e: React.MouseEvent<SVGSVGElement>
  ) => {
    setIsHovered(true);
    onMouseEnter?.(e);
  };

  const handleMouseLeave = (
    e: React.MouseEvent<SVGSVGElement>
  ) => {
    setIsHovered(false);
    onMouseLeave?.(e);
  };

  const animationClass = `icon-animation-${animation}`;
  const hoverClass = isHovered ? 'icon-hovered' : '';
  const combinedClassName =
    `${className} ${animationClass} ${hoverClass}`.trim();

  return (
    <IconFactory
      {...props}
      className={combinedClassName}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      style={{
        transitionDuration: `${duration}ms`,
        ...props.style,
      }}
    />
  );
};
css/* アニメーション効果のCSS */
.icon-animation-hover-scale {
  transition: transform 0.2s ease;
}

.icon-animation-hover-scale.icon-hovered {
  transform: scale(1.1);
}

.icon-animation-hover-rotate {
  transition: transform 0.2s ease;
}

.icon-animation-hover-rotate.icon-hovered {
  transform: rotate(15deg);
}

.icon-animation-pulse {
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

.icon-animation-bounce {
  animation: bounce 1s infinite;
}

@keyframes bounce {
  0%,
  20%,
  50%,
  80%,
  100% {
    transform: translateY(0);
  }
  40% {
    transform: translateY(-10px);
  }
  60% {
    transform: translateY(-5px);
  }
}

レスポンシブアイコンシステム

画面サイズに応じてアイコンサイズを自動調整するシステムを実装します。

typescript// hooks/useResponsiveIconSize.ts
import { useState, useEffect } from 'react';
import { IconSize } from '@/components/icons/Icon';

interface BreakpointConfig {
  sm: number;
  md: number;
  lg: number;
  xl: number;
}

const defaultBreakpoints: BreakpointConfig = {
  sm: 640,
  md: 768,
  lg: 1024,
  xl: 1280,
};

export const useResponsiveIconSize = (
  sizeMap: Partial<
    Record<keyof BreakpointConfig | 'base', IconSize>
  > = {}
): IconSize => {
  const [windowWidth, setWindowWidth] = useState(0);

  useEffect(() => {
    const handleResize = () =>
      setWindowWidth(window.innerWidth);

    // 初期値設定
    handleResize();

    window.addEventListener('resize', handleResize);
    return () =>
      window.removeEventListener('resize', handleResize);
  }, []);

  // デフォルトサイズマッピング
  const defaultSizeMap: Record<
    keyof BreakpointConfig | 'base',
    IconSize
  > = {
    base: 'sm',
    sm: 'md',
    md: 'md',
    lg: 'lg',
    xl: 'lg',
    ...sizeMap,
  };

  // 現在の画面サイズに基づいてアイコンサイズを決定
  if (windowWidth >= defaultBreakpoints.xl)
    return defaultSizeMap.xl;
  if (windowWidth >= defaultBreakpoints.lg)
    return defaultSizeMap.lg;
  if (windowWidth >= defaultBreakpoints.md)
    return defaultSizeMap.md;
  if (windowWidth >= defaultBreakpoints.sm)
    return defaultSizeMap.sm;
  return defaultSizeMap.base;
};

// components/icons/ResponsiveIcon.tsx
import React from 'react';
import {
  IconFactory,
  IconFactoryProps,
} from './IconFactory';
import { useResponsiveIconSize } from '@/hooks/useResponsiveIconSize';
import { IconSize } from './Icon';

export interface ResponsiveIconProps
  extends Omit<IconFactoryProps, 'size'> {
  sizeMap?: Partial<
    Record<'base' | 'sm' | 'md' | 'lg' | 'xl', IconSize>
  >;
}

export const ResponsiveIcon: React.FC<
  ResponsiveIconProps
> = ({ sizeMap, ...props }) => {
  const responsiveSize = useResponsiveIconSize(sizeMap);

  return <IconFactory size={responsiveSize} {...props} />;
};

実践的な使用例

これまでに作成したコンポーネントを組み合わせた実践的な使用例を示します。

typescript// components/Navigation.tsx
import React from 'react';
import { AnimatedIcon } from '@/components/icons/AnimatedIcon';
import { ResponsiveIcon } from '@/components/icons/ResponsiveIcon';

const Navigation: React.FC = () => {
  return (
    <nav className='navigation'>
      <div className='nav-brand'>
        <ResponsiveIcon
          name='home'
          sizeMap={{ base: 'md', lg: 'lg' }}
          className='brand-icon'
        />
        <span>MyApp</span>
      </div>

      <div className='nav-menu'>
        <AnimatedIcon
          name='search'
          animation='hover-scale'
          size='md'
          className='nav-icon'
        />
        <AnimatedIcon
          name='notification'
          animation='pulse'
          size='md'
          className='nav-icon notification-icon'
        />
        <AnimatedIcon
          name='user'
          animation='hover-rotate'
          size='md'
          className='nav-icon profile-icon'
        />
      </div>
    </nav>
  );
};

// components/ActionButton.tsx
import React from 'react';
import { ThemedIcon } from '@/components/icons/ThemedIcon';
import { IconName } from '@/components/icons/IconFactory';

interface ActionButtonProps {
  icon: IconName;
  label: string;
  variant?: 'primary' | 'secondary' | 'danger';
  onClick?: () => void;
}

export const ActionButton: React.FC<ActionButtonProps> = ({
  icon,
  label,
  variant = 'primary',
  onClick,
}) => {
  return (
    <button
      className={`action-button action-button-${variant}`}
      onClick={onClick}
    >
      <ThemedIcon
        name={icon}
        size='sm'
        className='button-icon'
      />
      <span>{label}</span>
    </button>
  );
};

まとめ

この記事では、Vite を活用した SVG アイコンの効率的な管理と活用方法について詳しく解説しました。従来の画像形式の限界を理解し、SVG の優位性を活かした実践的なソリューションをご紹介できたのではないでしょうか。

特に重要なポイントをまとめると、以下のようになります。

技術的な成果:

  • vite-plugin-svgr を使った React コンポーネント化
  • 型安全な TypeScript 対応
  • Tree Shaking による最適化
  • 動的なスタイリングとアニメーション効果

開発効率の向上:

  • 統一されたアイコン管理システム
  • 再利用可能なコンポーネント設計
  • レスポンシブ対応の自動化
  • テーマ切り替えへの対応

保守性の確保:

  • 一元化されたエクスポート管理
  • 命名規則の統一
  • プレビュー機能による開発体験向上

これらの手法を活用することで、プロジェクトの規模が大きくなっても、一貫性を保ちながら効率的に SVG アイコンを管理できるようになります。また、パフォーマンスの最適化も同時に実現できるため、ユーザーエクスペリエンスの向上にも貢献するでしょう。

今後の Web 開発において、SVG アイコンの重要性はさらに高まっていくと予想されます。この記事でご紹介した手法をベースに、あなたのプロジェクトに最適なアイコンシステムを構築していただければと思います。

関連リンク