Dealing with SVG icons in Vue + Vite
Tags: vite, vue, svg, javascript
Published: 2022-10-24 06:31:41

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](https://www.npmjs.com/package/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>
```
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);
```
**Usage:**
```
<icon name="some-icon-name" />
```
**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](https://www.npmjs.com/package/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](https://www.npmjs.com/package/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
```

Next you need to add this plugin to your `vite.config.js`:
```
import svgLoader from 'vite-svg-loader';

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

});
```

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>
```

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" />
```

## 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>
```
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>
```
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" />
```

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

#### Support & follow me
| [![Buy me a coffee](https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/geowrgetudor) | [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/geowrgetudor) | [![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/geowrgetudor) | [![Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://linkedin.com/in/geowrgetudor) |
| ------------- | ------------- | ------------- | ------------- |