vue3 + vite project uses SVG icons

We often use some icons when developing a project, but some icons are not available in the limited icon library. Therefore, a small number of icons require us to search online or customize some special icons. Generally, there are few icon libraries that meet the needs of real-life project development. In this case, it is recommended to use the iconify icon library, which contains almost all our common icons and is very simple to use. Without further ado, let’s get started with actual combat.

Introducing the vite plug-in

  • unplugin-icons
  • unplugin-vue-components
  • vite-plugin-svg-icons
  • @iconify/json
  • @iconify/vue
# npm

npm i -D @iconify/json @iconify/vue unplugin-icons vite-plugin-svg-icons unplugin-vue-components

#yarn

yarn add -D @iconify/json @iconify/vue unplugin-icons vite-plugin-svg-icons unplugin-vue-components

#pnpm

pnpm add -D @iconify/json @iconify/vue unplugin-icons vite-plugin-svg-icons unplugin-vue-components

How to use

The first type: unplugin-icons

Configure vite plug-in:

 //Configure icons- unplugin-icons
Icons({
    compiler: "vue3",
    customCollections: {
       ["local"]: FileSystemIconLoader(LOCAL_PATH, (svg: string) =>
               svg.replace(/^<svg\s/, '<svg width="1em" height="1em" '))
       },
    scale: 1,
    autoInstall: true,
    defaultClass: "inline-block"
}),
//Configure unplugin-vue-components
Components({
    resolvers: [
        IconsResolver({
           prefix: "icon",
           customCollections: COLLECTION_NAME,
        }),
    ],
}),
  1. Use iconify icon as carrier display:
<!-- Using icons -->
<icon-ep-bicycle style="font-size: 30px;color: #0C0C0C;background-color: #efafaf"></icon-ep-bicycle>

Effect:

Icon explanation:

icon is our customized prefix

ep-bicycle is the name of the icon. You can go to iconify to query it and replace the colon with the horizontal bar -.

2. Use local svg icon as carrier display

Download the svg icon file and place it in the src/assets/icons/svg directory of the project

<!-- Using icons -->
<icon-local-acrobat class="icons"></icon-local-acrobat>
<icon-local-control-center class="icons"></icon-local-control-center>
<icon-local-setting class="icons"></icon-local-setting>

Effect:

The style of the icon can be adjusted by yourself. The style of the demonstration is customized by me.

Icon explanation:

<strong>icon-local-acrobat</strong>

icon is our customized prefix

local is our custom collection container name, specified when configuring vite

acrobat is the name of the icon file

Second type: vite-plugin-svg-icons

Configure vite plug-in

//Configure icons- vite-plugin-svg-icons
createSvgIconsPlugin({
    //Specify the icon folder that needs to be cached
    iconDirs: [LOCAL_PATH],
    //Specify symbolId format
    symbolId: `icon-[dir]-[name]`,
    inject: "body-last",
    customDomId: "__SVG_ICON_LOCAL__",
})

Introduce styles in main.ts

import "virtual:svg-icons-register";

Customizing the SvgIcon component facilitates the use of icons. Here I use the component defined in the form of jsx. The component defined in vue can be written by referring to the following form.

import type {CSSProperties, PropType} from "vue";
import {computed, defineComponent} from "vue";
import {Icon} from "@iconify/vue";

//horizontal: flip horizontally, vertical: flip vertically
type FlipType = "horizontal" | "vertical";
//The number represents the rotation angle 0° 90° 180° 270°
type RotateType = 0 | 1 | 2 | 3;

/**
 * SvgIcon component
 * @date 2023/9/26
 */
export default defineComponent({
    name: "SvgIcon",
    props: {
        //Icon name
        icon: {
            type: String,
            required: true,
            default: "",
        },
        // Whether it is a local svg icon
        isLocal: {
            type: Boolean,
            default: false,
        },
        //Icon size
        size: {
            type: [Number, String],
            default: 14,
        },
        // Rotation angle
        rotate: {
            type: Number as PropType<RotateType>,
            default: 0,
        },
        // Icon flip (horizontal flip, vertical flip)
        flip: String as PropType<FlipType>,
        // Icon rotation animation effect
        spin: {
            type: Boolean,
            default: false,
        },
        // Individual icon style
        iconStyle: Object as PropType<CSSProperties>,
        //icon color
        color: String,
        // Click the icon to trigger the event
        onClick: Function as PropType<(e: MouseEvent) => void>,
    },
    setup(props, {attrs}) {
        const bindAttrs = computed<{ class: string; style: string }>(() => ({
            class: (attrs.class as string) || "",
            style: (attrs.style as string) || "",
        }));

        const symbolId = () => `#icon-${props.icon}`;

        const rotate = [0, 90, 180, 270];

        const localIconStyle = {
            width: "1em",
            height: "1em",
            lineHeight: "1em",
            fontSize: `${`${props.size}`.replace("px", "")}px`,
            animation: props.spin ? "circle 3s infinite linear" : "",
            transform: (() => {
                const scale = props.flip
                    ? props.flip == "vertical"
                        ? "scaleY(-1)"
                        : props.flip == "horizontal"
                            ? "scaleX(-1)"
                            : ""
                    : "";
                return `rotate(${rotate[props.rotate]}deg) ${scale}`;
            })(),
        };

        const iconStyles = {outline: "none", animation: props.spin ? "circle 3s infinite linear" : ""};
        return () => (
            <b
                onClick={e => {
                    props.onClick?.(e);
                }}
                class={["inline-block", bindAttrs.value.class, props.onClick ? "cursor-pointer" : ""]}
                style={bindAttrs.value.style}
            >
                {props.isLocal ? (
                    <svg aria-hidden="true"
                         style={props.iconStyle ? Object.assign(localIconStyle, props.iconStyle) : localIconStyle}>
                        <use xlinkHref={symbolId()} fill="currentColor"/>
                    </svg>
                ) : (
                    <Icon
                        icon={props.icon}
                        width={props.size}
                        height={props.size}
                        rotate={props.rotate}
                        flip={props.flip}
                        color={props.color}
                        style={props.iconStyle ? Object.assign(iconStyles, props.iconStyle) : iconStyles}
                    />
                )}
            </b>
        );
    },
});

Use iconify icon

<!-- Using icons -->
<SvgIcon icon="ep:avatar" size="50" color="#000"></SvgIcon>

Use local icons

<!-- Using icons -->
<SvgIcon icon="setting" size="50" is-local class="icons"></SvgIcon>

The difference is that when using local svg icons, isLocal needs to be set to true. You can explore other properties by yourself. There are also corresponding code comments in the component.

Complete code

vite configuration:

import {fileURLToPath, URL} from 'node:url'

import {defineConfig} from 'vite'
import path from "path";
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Icons from "unplugin-icons/vite";
import {FileSystemIconLoader} from "unplugin-icons/loaders";
import {createSvgIconsPlugin} from "vite-plugin-svg-icons";
import Components from "unplugin-vue-components/vite";
import IconsResolver from 'unplugin-icons/resolver';

/**
 * Get the project root path
 * @description without trailing slash
 */
export function getRootPath() {
    return path.resolve(process.cwd());
}

/**
 * Get the project src path
 * @param srcName - src directory name (default: "src")
 * @description without trailing slash
 */
export function getSrcPath(srcName = "src") {
    const rootPath = getRootPath();
    return `${rootPath}/${srcName}`;
}

const LOCAL_PATH = `${getSrcPath()}/assets/icons/svg`
const COLLECTION_NAME = "local"

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),
        vueJsx(),
        //Configure unplugin-vue-components
        Components({
            resolvers: [
                IconsResolver({
                    prefix: "icon",
                    customCollections: COLLECTION_NAME,
                }),
            ],
        }),
        // Configure icons - unplugin-icons
        Icons({
            compiler: "vue3",
            customCollections: {
                [COLLECTION_NAME]: FileSystemIconLoader(LOCAL_PATH, (svg: string) =>
                    svg.replace(/^<svg\s/, '<svg width="1em" height="1em" '),
                ),
            },
            scale: 1,
            autoInstall: true,
            defaultClass: "inline-block",
        }),
        // Configure icons - vite-plugin-svg-icons
        createSvgIconsPlugin({
            //Specify the icon folder that needs to be cached
            iconDirs: [LOCAL_PATH],
            //Specify symbolId format
            symbolId: `icon-[dir]-[name]`,
            inject: "body-last",
            customDomId: "__SVG_ICON_LOCAL__",
        })
    ],
    resolve: {
        alias: {
            '@':
                fileURLToPath(new URL('./src', import.meta.url))
        }
    }
})

SvgIcon component:

import type {CSSProperties, PropType} from "vue";
import {computed, defineComponent} from "vue";
import {Icon} from "@iconify/vue";

//horizontal: flip horizontally, vertical: flip vertically
type FlipType = "horizontal" | "vertical";
//The number represents the rotation angle 0° 90° 180° 270°
type RotateType = 0 | 1 | 2 | 3;

/**
 * SvgIcon component
 * @date 2023/9/26
 */
export default defineComponent({
    name: "SvgIcon",
    props: {
        //Icon name
        icon: {
            type: String,
            required: true,
            default: "",
        },
        // Whether it is a local svg icon
        isLocal: {
            type: Boolean,
            default: false,
        },
        //Icon size
        size: {
            type: [Number, String],
            default: 14,
        },
        // Rotation angle
        rotate: {
            type: Number as PropType<RotateType>,
            default: 0,
        },
        // Icon flip (horizontal flip, vertical flip)
        flip: String as PropType<FlipType>,
        // Icon rotation animation effect
        spin: {
            type: Boolean,
            default: false,
        },
        // Individual icon style
        iconStyle: Object as PropType<CSSProperties>,
        //icon color
        color: {
            type: String,
            default: "#000",
        },
        // Click the icon to trigger the event
        onClick: Function as PropType<(e: MouseEvent) => void>,
    },
    setup(props, {attrs}) {
        const bindAttrs = computed<{ class: string; style: string }>(() => ({
            class: (attrs.class as string) || "",
            style: (attrs.style as string) || "",
        }));

        const symbolId = () => `#icon-${props.icon}`;

        const rotate = [0, 90, 180, 270];

        const localIconStyle = {
            width: "1em",
            height: "1em",
            lineHeight: "1em",
            fontSize: `${`${props.size}`.replace("px", "")}px`,
            animation: props.spin ? "circle 3s infinite linear" : "",
            transform: (() => {
                const scale = props.flip
                    ? props.flip == "vertical"
                        ? "scaleY(-1)"
                        : props.flip == "horizontal"
                            ? "scaleX(-1)"
                            : ""
                    : "";
                return `rotate(${rotate[props.rotate]}deg) ${scale}`;
            })(),
        };

        const iconStyles = {outline: "none", animation: props.spin ? "circle 3s infinite linear" : ""};
        return () => (
            <b
                onClick={e => {
                    props.onClick?.(e);
                }}
                class={["inline-block", bindAttrs.value.class, props.onClick ? "cursor-pointer" : ""]}
                style={bindAttrs.value.style}
            >
                {props.isLocal ? (
                    <svg aria-hidden="true"
                         style={props.iconStyle ? Object.assign(localIconStyle, props.iconStyle) : localIconStyle}>
                        <use xlinkHref={symbolId()} fill="currentColor"/>
                    </svg>
                ) : (
                    <Icon
                        icon={props.icon}
                        width={props.size}
                        height={props.size}
                        rotate={props.rotate}
                        flip={props.flip}
                        color={props.color}
                        style={props.iconStyle ? Object.assign(iconStyles, props.iconStyle) : iconStyles}
                    />
                )}
            </b>
        );
    },
});

main.ts introduction

import "virtual:svg-icons-register";

use:

<script setup lang="ts">
import SvgIcon from "@/components/SvgIcon";
</script>

<template>
  <main style="display: flex;flex-direction: column">
    <!-- Using icons -->
    <icon-ep-bicycle class="icons"></icon-ep-bicycle>

    <!-- Using icons -->
    <icon-local-acrobat class="icons"></icon-local-acrobat>
    <icon-local-control-center class="icons"></icon-local-control-center>
    <icon-local-setting class="icons"></icon-local-setting>


    <!-- Using icons -->
    <SvgIcon icon="ep:avatar" size="50"></SvgIcon>

    <!-- Using icons -->
    <SvgIcon icon="setting" size="50" is-local class="icons"></SvgIcon>
  </main>
</template>

<style scoped>
.icons {
  font-size: 30px;
  color: #0C0C0C;
  background-color: #efafaf;
  margin: 20px;
}
</style>