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?

Support & follow me

Buy me a coffee Twitter GitHub Linkedin

Top comments (7)

Collapse
 
laygir profile image
Liger

Hey, thanks for the post!

I am also using vite-svg-loader but my svg's are divided into sub folders, like /svg/icons, /svg/illustrations, /svg/badges` an so on..

When using vite-svg-loader I can't seem to make it work with subfolders.. Would you know how to achieve that?

Collapse
 
amitsoni7 profile image
Amit Kumar Soni • Edited

I have resolved in issue with javascript. I know it is not the optimum solution but it's worked for me.

let name = props.name.split('/');

const SvgVue = defineAsyncComponent(() =>
  import(`../svg/${name[0]}/${name[1]}.svg`)
);
Enter fullscreen mode Exit fullscreen mode

only limitation is it works for only single level of folder like /svg/illustrations.xyz.svg

Collapse
 
laygir profile image
Liger • Edited

Thank you for the follow-up!

I ended up like this.. glob helps to look through nested folders and not sure how performant this is but so far it's been working well to my surprise..

export function createSvgMap() {
  function getSvgNameFromPath(path) {
    const pathSplit = path.split('/');
    const fileName = pathSplit[pathSplit.length - 1] || '';
    const svgName = fileName.replace('.svg', '');

    return svgName;
  }

  const modules = import.meta.glob('@/assets/svg/**/*.svg', {
    import: 'default',
    eager: true,
  });

  const svgMap = new Map();

  Object.keys(modules).forEach((path) => {
    const svgName = getSvgNameFromPath(path);
    svgMap.set(svgName, modules[path]);
  });

  return svgMap;
}
Enter fullscreen mode Exit fullscreen mode

in SvgBase.vue component

<template>
  <component :is="getSvg" />
</template>
Enter fullscreen mode Exit fullscreen mode
import { h } from 'vue';

export default {
  computed: {
    getSvg() {
      const svg = createSvgMap().get(this.svgPath);
      return svg && h(svg);
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Then use it like (notice only file name is given, createSvgMap will have all svg's in the defined directory path)

<my-svg-base svg="icon-loading" />
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mortezasabihi profile image
Morteza Sabihi

Thanks for you're great article. I have done the solution 3 in my Vite project and i found out it doesn't support dynamic component and loads all of the icon components. (Network Tab)

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

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>