DEV Community 👩‍💻👨‍💻

Cover image for Dealing with SVG icons in Vue + Vite
George Tudor
George Tudor

Posted on • Updated on

Dealing with SVG icons in Vue + Vite

Recently I've converted a Vue project that was using Webpack to Vite. All went smooth until I've discovered that Vite doesn't support require() out of the box.
I'm using some custom SVG icons that I'm keeping in a folder call obviously svg. With Webpack it was easy to import them and I had several alternatives on how to do it.
To load them I was using vue-inline-svg plus a custom Vue component that wraps around this package:

<template>
    <InlineSvg
        :src="require(`@/svg/${name}.svg`)"
        class="fill-current"
    />
</template>

<script>
import InlineSvg from "vue-inline-svg";

export default {
    components: { InlineSvg },
    props: {
        name: {
            type: String,
            required: true,
        },
    },
};
</script>
Enter fullscreen mode Exit fullscreen mode

This is just a simple custom Vue component that accepts a prop called name and loads <InlineSvg /> under the hood.
As a side note, my custom component called icon is defined as a global component inside the app.js file, so I can use it all over the place:

import Icon from "@/components/Icon.vue"; 

const app = createApp({ render: () => h(App, props) })
            .component("icon", Icon);
Enter fullscreen mode Exit fullscreen mode

Usage:

<icon name="some-icon-name" />
Enter fullscreen mode Exit fullscreen mode

This approach was simple, but unfortunately it's NOT working out of the box with Vite, so let's fix it.

Solution 1: Adding back require()

Probably the easiest way to fix this is to use vite-plugin-require which adds support for require() to Vite.
You should not see any breaking changes while using this and I believe it's the most elegant way to do it if you're already using require() to load your SVGs.

Update: I've noticed a bug while using this method in Vite 3.x

Solution 2: Importing SVGs as components

This is what I personally use and I think it's the most elegant way of doing it.

If you are not using any SVG loader or/and a custom Vue component as a wrapper, you can use vite-svg-loader which allows you to tap straight into the SVG and import it as a Vue component.
More than that you can go a step forward and a custom wrapper over it to achieve something similar with the other 2 solutions, by making use of dynamic components.

To set it up you first need to install it:

npm i vite-svg-loader
Enter fullscreen mode Exit fullscreen mode

Next you need to add this plugin to your vite.config.js:

import svgLoader from 'vite-svg-loader';

export default defineConfig({
  plugins: [
    // ...
    svgLoader(),
  ],

});
Enter fullscreen mode Exit fullscreen mode

Now you can import each SVG as component, url or raw... but importing each SVG individually will make a huge mess into our project.

To fix this issue, we can create a component that will dynamically load the SVG we want:

<script setup>
import { defineAsyncComponent } from 'vue';

const props = defineProps({
  name: {
    type: String,
    required: true,
  },
});

const icon = defineAsyncComponent(() =>
  import(`../assets/svg/${props.name}.svg`)
);
</script>

<template>
  <component :is="icon" class="fill-current" />
</template>
Enter fullscreen mode Exit fullscreen mode

Now you can use this new component by importing it whenever you need it in your project or add it as a global into your app.js.

<icon name="user" />
Enter fullscreen mode Exit fullscreen mode

Solution 3: Adding your SVGs as Vue components

While I do not recommend this, it might be helpful if you have just a few icons you need to include and reuse here and there. This is more or less a brainless approach, but it works if you're lazy.
So what you can do is to define your SVGs into separate Vue components. Example this Check.vue placed inside the icons folder:

<template>
    <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 32 32"
        class="fill-current"
    >
        <path
            d="M 28.28125 6.28125 L 11 23.5625 L 3.71875 16.28125 L 2.28125 17.71875 L 10.28125 25.71875 L 11 26.40625 L 11.71875 25.71875 L 29.71875 7.71875 Z"
        />
    </svg>
</template>
Enter fullscreen mode Exit fullscreen mode

Now we also need to have a helper component called <Icon /> that we can define as global. Inside this we can make use of dynamic components and load the .vue SVGs dynamically:

<template>
    <component :is="dynamicComponent" />
</template>

<script>
import { defineAsyncComponent } from "vue";

export default {
    props: {
        name: {
            type: String,
            required: true,
        },
    },

    computed: {
        dynamicComponent() {
            const name = this.name.charAt(0).toUpperCase() + this.name.slice(1);

            return defineAsyncComponent(() => import(`./icons/${name}.vue`));
        },
    },
};
</script>
Enter fullscreen mode Exit fullscreen mode

To use the global component, you can do the same thing as in the legacy code I've presented in the begining:

<icon name="check" />
Enter fullscreen mode Exit fullscreen mode

That's pretty much it. What method do you prefer?

Top comments (3)

Collapse
 
lambrero profile image
Avin Lambrero

Thanks for tut!

Collapse
 
donghoon759 profile image
Donghoon Song

Do you have any idea to import remote svg files inline? not to bundle any svg file.

Collapse
 
mankowitz profile image
Scott Mankowitz • Edited on

What if you want include it as part of the section of a single page component?</p> <p>.temporary-patient {<br> background-image: url(&quot;/svg/temp-patient.svg&quot;);<br> background-color: yellow;<br> }</p>

12 APIs That You Will Love

>> Check out this classic DEV post <<